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
, NWBase
, get_first_enabled_output
, is_visible_socket
, viewer_socket_name
)
35 class NWLazyMix(Operator
, NWBase
):
36 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
37 bl_idname
= "node.nw_lazy_mix"
38 bl_label
= "Mix Nodes"
39 bl_options
= {'REGISTER', 'UNDO'}
41 def modal(self
, context
, event
):
42 context
.area
.tag_redraw()
43 nodes
, links
= get_nodes_links(context
)
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
):
248 if nw_check(context
):
249 if 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
):
339 if nw_check(context
):
340 if context
.selected_nodes
:
341 valid
= len(context
.selected_nodes
) <= 2
344 def execute(self
, context
):
345 nodes
, links
= get_nodes_links(context
)
346 selected_nodes
= context
.selected_nodes
347 n1
= selected_nodes
[0]
350 if len(selected_nodes
) == 2:
351 n2
= selected_nodes
[1]
352 if n1
.outputs
and n2
.outputs
:
357 for output
in n1
.outputs
:
359 for link
in output
.links
:
360 n1_outputs
.append([out_index
, link
.to_socket
])
365 for output
in n2
.outputs
:
367 for link
in output
.links
:
368 n2_outputs
.append([out_index
, link
.to_socket
])
372 for connection
in n1_outputs
:
374 connect_sockets(n2
.outputs
[connection
[0]], connection
[1])
376 self
.report({'WARNING'},
377 "Some connections have been lost due to differing numbers of output sockets")
378 for connection
in n2_outputs
:
380 connect_sockets(n1
.outputs
[connection
[0]], connection
[1])
382 self
.report({'WARNING'},
383 "Some connections have been lost due to differing numbers of output sockets")
385 if n1
.outputs
or n2
.outputs
:
386 self
.report({'WARNING'}, "One of the nodes has no outputs!")
388 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
391 elif len(selected_nodes
) == 1:
392 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
393 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
399 if i1
.is_linked
and not i1
.is_multi_input
:
402 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
404 types
.append([i1
, similar_types
, i
])
406 types
.sort(key
=lambda k
: k
[1], reverse
=True)
412 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
414 i1f
= pair
[0].links
[0].from_socket
415 i1t
= pair
[0].links
[0].to_socket
416 i2f
= pair
[1].links
[0].from_socket
417 i2t
= pair
[1].links
[0].to_socket
418 connect_sockets(i1f
, i2t
)
419 connect_sockets(i2f
, i1t
)
422 fs
= t
[0].links
[0].from_socket
424 links
.remove(t
[0].links
[0])
425 if i
+ 1 == len(n1
.inputs
):
428 while n1
.inputs
[i
].is_linked
:
430 connect_sockets(fs
, n1
.inputs
[i
])
431 elif len(types
) == 2:
432 i1f
= types
[0][0].links
[0].from_socket
433 i1t
= types
[0][0].links
[0].to_socket
434 i2f
= types
[1][0].links
[0].from_socket
435 i2t
= types
[1][0].links
[0].to_socket
436 connect_sockets(i1f
, i2t
)
437 connect_sockets(i2f
, i1t
)
440 self
.report({'WARNING'}, "This node has no input connections to swap!")
442 self
.report({'WARNING'}, "This node has no inputs to swap!")
444 force_update(context
)
448 class NWResetBG(Operator
, NWBase
):
449 """Reset the zoom and position of the background image"""
450 bl_idname
= 'node.nw_bg_reset'
451 bl_label
= 'Reset Backdrop'
452 bl_options
= {'REGISTER', 'UNDO'}
455 def poll(cls
, context
):
457 if nw_check(context
):
458 snode
= context
.space_data
459 valid
= snode
.tree_type
== 'CompositorNodeTree'
462 def execute(self
, context
):
463 context
.space_data
.backdrop_zoom
= 1
464 context
.space_data
.backdrop_offset
[0] = 0
465 context
.space_data
.backdrop_offset
[1] = 0
469 class NWAddAttrNode(Operator
, NWBase
):
470 """Add an Attribute node with this name"""
471 bl_idname
= 'node.nw_add_attr_node'
472 bl_label
= 'Add UV map'
473 bl_options
= {'REGISTER', 'UNDO'}
475 attr_name
: StringProperty()
477 def execute(self
, context
):
478 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
479 nodes
, links
= get_nodes_links(context
)
480 nodes
.active
.attribute_name
= self
.attr_name
484 class NWPreviewNode(Operator
, NWBase
):
485 bl_idname
= "node.nw_preview_node"
486 bl_label
= "Preview Node"
487 bl_description
= "Connect active node to the Node Group output or the Material Output"
488 bl_options
= {'REGISTER', 'UNDO'}
490 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
491 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
492 run_in_geometry_nodes
: BoolProperty(default
=True)
495 self
.shader_output_type
= ""
496 self
.shader_output_ident
= ""
499 def poll(cls
, context
):
500 if nw_check(context
):
501 space
= context
.space_data
502 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
503 if context
.active_node
:
504 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
511 def get_output_sockets(cls
, node_tree
):
512 return [item
for item
in node_tree
.interface
.items_tree
if item
.item_type
== 'SOCKET' and item
.in_out
in {'OUTPUT', 'BOTH'}]
514 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
515 # check if a viewer output already exists in a node group otherwise create
516 if hasattr(node
, "node_tree"):
518 output_sockets
= self
.get_output_sockets(node
.node_tree
)
519 if len(output_sockets
):
521 for i
, socket
in enumerate(output_sockets
):
522 if is_viewer_socket(socket
) and socket
.socket_type
== socket_type
:
523 # if viewer output is already used but leads to the same socket we can still use it
524 is_used
= self
.is_socket_used_other_mats(socket
)
526 if connect_socket
is None:
528 groupout
= get_group_output_node(node
.node_tree
)
529 groupout_input
= groupout
.inputs
[i
]
530 links
= groupout_input
.links
531 if connect_socket
not in [link
.from_socket
for link
in links
]:
533 viewer_socket
= socket
537 if not viewer_socket
and free_socket
:
538 viewer_socket
= free_socket
540 if not viewer_socket
:
541 # create viewer socket
542 viewer_socket
= node
.node_tree
.interface
.new_socket(viewer_socket_name
, in_out
='OUTPUT', socket_type
=socket_type
)
543 viewer_socket
.NWViewerSocket
= True
546 def init_shader_variables(self
, space
, shader_type
):
547 if shader_type
== 'OBJECT':
548 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
549 self
.shader_output_type
= "OUTPUT_MATERIAL"
550 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
552 self
.shader_output_type
= "OUTPUT_LIGHT"
553 self
.shader_output_ident
= "ShaderNodeOutputLight"
555 elif shader_type
== 'WORLD':
556 self
.shader_output_type
= "OUTPUT_WORLD"
557 self
.shader_output_ident
= "ShaderNodeOutputWorld"
559 def get_shader_output_node(self
, tree
):
560 for node
in tree
.nodes
:
561 if node
.type == self
.shader_output_type
and node
.is_active_output
:
565 def ensure_group_output(cls
, tree
):
566 # check if a group output node exists otherwise create
567 groupout
= get_group_output_node(tree
)
569 groupout
= tree
.nodes
.new('NodeGroupOutput')
570 loc_x
, loc_y
= get_output_location(tree
)
571 groupout
.location
.x
= loc_x
572 groupout
.location
.y
= loc_y
573 groupout
.select
= False
574 # So that we don't keep on adding new group outputs
575 groupout
.is_active_output
= True
579 def search_sockets(cls
, node
, sockets
, index
=None):
580 # recursively scan nodes for viewer sockets and store in list
581 for i
, input_socket
in enumerate(node
.inputs
):
582 if index
and i
!= index
:
584 if len(input_socket
.links
):
585 link
= input_socket
.links
[0]
586 next_node
= link
.from_node
587 external_socket
= link
.from_socket
588 if hasattr(next_node
, "node_tree"):
589 for socket_index
, socket
in enumerate(next_node
.node_tree
.interface
.items_tree
):
590 if socket
.identifier
== external_socket
.identifier
:
592 if is_viewer_socket(socket
) and socket
not in sockets
:
593 sockets
.append(socket
)
594 # continue search inside of node group but restrict socket to where we came from
595 groupout
= get_group_output_node(next_node
.node_tree
)
596 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
599 def scan_nodes(cls
, tree
, sockets
):
600 # get all viewer sockets in a material tree
601 for node
in tree
.nodes
:
602 if hasattr(node
, "node_tree"):
603 if node
.node_tree
is None:
605 for socket
in cls
.get_output_sockets(node
.node_tree
):
606 if is_viewer_socket(socket
) and (socket
not in sockets
):
607 sockets
.append(socket
)
608 cls
.scan_nodes(node
.node_tree
, sockets
)
611 def remove_socket(cls
, tree
, socket
):
612 interface
= tree
.interface
613 interface
.remove(socket
)
614 interface
.active_index
= min(interface
.active_index
, len(interface
.items_tree
) - 1)
616 def link_leads_to_used_socket(self
, link
):
617 # return True if link leads to a socket that is already used in this material
618 socket
= get_internal_socket(link
.to_socket
)
619 return (socket
and self
.is_socket_used_active_mat(socket
))
621 def is_socket_used_active_mat(self
, socket
):
622 # ensure used sockets in active material is calculated and check given socket
623 if not hasattr(self
, "used_viewer_sockets_active_mat"):
624 self
.used_viewer_sockets_active_mat
= []
625 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
627 self
.search_sockets(materialout
, self
.used_viewer_sockets_active_mat
)
628 return socket
in self
.used_viewer_sockets_active_mat
630 def is_socket_used_other_mats(self
, socket
):
631 # ensure used sockets in other materials are calculated and check given socket
632 if not hasattr(self
, "used_viewer_sockets_other_mats"):
633 self
.used_viewer_sockets_other_mats
= []
634 for mat
in bpy
.data
.materials
:
635 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
638 materialout
= self
.get_shader_output_node(mat
.node_tree
)
640 self
.search_sockets(materialout
, self
.used_viewer_sockets_other_mats
)
641 return socket
in self
.used_viewer_sockets_other_mats
643 def invoke(self
, context
, event
):
644 space
= context
.space_data
645 # Ignore operator when running in wrong context.
646 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
647 return {'PASS_THROUGH'}
649 shader_type
= space
.shader_type
650 self
.init_shader_variables(space
, shader_type
)
651 mlocx
= event
.mouse_region_x
652 mlocy
= event
.mouse_region_y
653 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
654 if 'FINISHED' in select_node
: # only run if mouse click is on a node
655 active_tree
, path_to_tree
= get_active_tree(context
)
656 nodes
, links
= active_tree
.nodes
, active_tree
.links
657 base_node_tree
= space
.node_tree
658 active
= nodes
.active
660 # For geometry node trees we just connect to the group output
661 if space
.tree_type
== "GeometryNodeTree":
664 for out
in active
.outputs
:
665 if is_visible_socket(out
):
674 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
675 self
.scan_nodes(base_node_tree
, delete_sockets
)
677 # Find (or create if needed) the output of this node tree
678 geometryoutput
= self
.ensure_group_output(base_node_tree
)
680 # Analyze outputs, make links
683 for i
, out
in enumerate(active
.outputs
):
684 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
685 valid_outputs
.append(i
)
687 out_i
= valid_outputs
[0] # Start index of node's outputs
688 for i
, valid_i
in enumerate(valid_outputs
):
689 for out_link
in active
.outputs
[valid_i
].links
:
690 if is_viewer_link(out_link
, geometryoutput
):
691 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
692 if i
< len(valid_outputs
) - 1:
693 out_i
= valid_outputs
[i
+ 1]
695 out_i
= valid_outputs
[0]
697 make_links
= [] # store sockets for new links
699 # If there is no 'GEOMETRY' output type - We can't preview the node
702 socket_type
= 'GEOMETRY'
703 # Find an input socket of the output of type geometry
704 geometryoutindex
= None
705 for i
, inp
in enumerate(geometryoutput
.inputs
):
706 if inp
.type == socket_type
:
709 if geometryoutindex
is None:
710 # Create geometry socket
711 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
712 geometryoutindex
= len(geometryoutput
.inputs
) - 1
714 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
715 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
716 for li_from
, li_to
in make_links
:
717 connect_sockets(li_from
, li_to
)
719 # Create links through node groups until we reach the active node
720 tree
= base_node_tree
721 link_end
= output_socket
722 while tree
.nodes
.active
!= active
:
723 node
= tree
.nodes
.active
724 viewer_socket
= self
.ensure_viewer_socket(
725 node
, 'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
726 link_start
= node
.outputs
[viewer_socket
.identifier
]
727 node_socket
= viewer_socket
728 if node_socket
in delete_sockets
:
729 delete_sockets
.remove(node_socket
)
730 connect_sockets(link_start
, link_end
)
732 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[viewer_socket
.identifier
]
733 tree
= tree
.nodes
.active
.node_tree
734 connect_sockets(active
.outputs
[out_i
], link_end
)
737 for socket
in delete_sockets
:
738 tree
= socket
.id_data
739 self
.remove_socket(tree
, socket
)
741 nodes
.active
= active
743 force_update(context
)
746 # What follows is code for the shader editor
749 for out
in active
.outputs
:
750 if is_visible_socket(out
):
754 # get material_output node
755 materialout
= None # placeholder node
758 # scan through all nodes in tree including nodes inside of groups to find viewer sockets
759 self
.scan_nodes(base_node_tree
, delete_sockets
)
761 materialout
= self
.get_shader_output_node(base_node_tree
)
763 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
764 materialout
.location
= get_output_location(base_node_tree
)
765 materialout
.select
= False
769 for i
, out
in enumerate(active
.outputs
):
770 if is_visible_socket(out
):
771 valid_outputs
.append(i
)
773 out_i
= valid_outputs
[0] # Start index of node's outputs
774 for i
, valid_i
in enumerate(valid_outputs
):
775 for out_link
in active
.outputs
[valid_i
].links
:
776 if is_viewer_link(out_link
, materialout
):
777 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
778 if i
< len(valid_outputs
) - 1:
779 out_i
= valid_outputs
[i
+ 1]
781 out_i
= valid_outputs
[0]
783 make_links
= [] # store sockets for new links
785 socket_type
= 'NodeSocketShader'
786 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
787 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
788 output_socket
= materialout
.inputs
[materialout_index
]
789 for li_from
, li_to
in make_links
:
790 connect_sockets(li_from
, li_to
)
792 # Create links through node groups until we reach the active node
793 tree
= base_node_tree
794 link_end
= output_socket
795 while tree
.nodes
.active
!= active
:
796 node
= tree
.nodes
.active
797 viewer_socket
= self
.ensure_viewer_socket(
798 node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
799 link_start
= node
.outputs
[viewer_socket
.identifier
]
800 node_socket
= viewer_socket
801 if node_socket
in delete_sockets
:
802 delete_sockets
.remove(node_socket
)
803 connect_sockets(link_start
, link_end
)
805 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[viewer_socket
.identifier
]
806 tree
= tree
.nodes
.active
.node_tree
807 connect_sockets(active
.outputs
[out_i
], link_end
)
810 for socket
in delete_sockets
:
811 if not self
.is_socket_used_other_mats(socket
):
812 tree
= socket
.id_data
813 self
.remove_socket(tree
, socket
)
815 nodes
.active
= active
818 force_update(context
)
825 class NWFrameSelected(Operator
, NWBase
):
826 bl_idname
= "node.nw_frame_selected"
827 bl_label
= "Frame Selected"
828 bl_description
= "Add a frame node and parent the selected nodes to it"
829 bl_options
= {'REGISTER', 'UNDO'}
831 label_prop
: StringProperty(
833 description
='The visual name of the frame node',
836 use_custom_color_prop
: BoolProperty(
838 description
="Use custom color for the frame node",
841 color_prop
: FloatVectorProperty(
843 description
="The color of the frame node",
844 default
=(0.604, 0.604, 0.604),
845 min=0, max=1, step
=1, precision
=3,
846 subtype
='COLOR_GAMMA', size
=3
849 def draw(self
, context
):
851 layout
.prop(self
, 'label_prop')
852 layout
.prop(self
, 'use_custom_color_prop')
853 col
= layout
.column()
854 col
.active
= self
.use_custom_color_prop
855 col
.prop(self
, 'color_prop', text
="")
857 def execute(self
, context
):
858 nodes
, links
= get_nodes_links(context
)
862 selected
.append(node
)
864 bpy
.ops
.node
.add_node(type='NodeFrame')
866 frm
.label
= self
.label_prop
867 frm
.use_custom_color
= self
.use_custom_color_prop
868 frm
.color
= self
.color_prop
870 for node
in selected
:
876 class NWReloadImages(Operator
):
877 bl_idname
= "node.nw_reload_images"
878 bl_label
= "Reload Images"
879 bl_description
= "Update all the image nodes to match their files on disk"
882 def poll(cls
, context
):
884 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
885 if context
.active_node
is not None:
886 for out
in context
.active_node
.outputs
:
887 if is_visible_socket(out
):
892 def execute(self
, context
):
893 nodes
, links
= get_nodes_links(context
)
894 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
897 if node
.type in image_types
:
898 if node
.type == "TEXTURE":
899 if node
.texture
: # node has texture assigned
900 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
901 if node
.texture
.image
: # texture has image assigned
902 node
.texture
.image
.reload()
910 self
.report({'INFO'}, "Reloaded images")
911 print("Reloaded " + str(num_reloaded
) + " images")
912 force_update(context
)
915 self
.report({'WARNING'}, "No images found to reload in this node tree")
919 class NWMergeNodes(Operator
, NWBase
):
920 bl_idname
= "node.nw_merge_nodes"
921 bl_label
= "Merge Nodes"
922 bl_description
= "Merge Selected Nodes"
923 bl_options
= {'REGISTER', 'UNDO'}
927 description
="All possible blend types, boolean operations and math operations",
928 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
],
930 merge_type
: EnumProperty(
932 description
="Type of Merge to be used",
934 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
935 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
936 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
937 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
938 ('MATH', 'Math Node', 'Merge using Math Nodes'),
939 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
940 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
944 # Check if the link connects to a node that is in selected_nodes
945 # If not, then check recursively for each link in the nodes outputs.
946 # If yes, return True. If the recursion stops without finding a node
947 # in selected_nodes, it returns False. The depth is used to prevent
948 # getting stuck in a loop because of an already present cycle.
950 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
952 # We're stuck in a cycle, but that cycle was already present,
953 # so we return False.
954 # NOTE: The number 255 is arbitrary, but seems to work well.
957 if node
in selected_nodes
:
961 for output
in node
.outputs
:
963 for olink
in output
.links
:
964 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
966 # None of the outputs found a node in selected_nodes, so there is no cycle.
969 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
970 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
971 # be connected. The last one is assumed to be a multi input socket.
972 # For convenience the node is returned.
974 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
975 # The y-location of the last node
976 loc_y
= nodes_list
[-1][2]
977 if merge_position
== 'CENTER':
978 # Average the y-location
979 for i
in range(len(nodes_list
) - 1):
980 loc_y
+= nodes_list
[i
][2]
981 loc_y
= loc_y
/ len(nodes_list
)
982 new_node
= nodes
.new(node_name
)
983 new_node
.hide
= do_hide
984 new_node
.location
.x
= loc_x
985 new_node
.location
.y
= loc_y
986 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
988 outputs_for_multi_input
= []
989 for i
, node
in enumerate(selected_nodes
):
991 # Search for the first node which had output links that do not create
992 # a cycle, which we can then reconnect afterwards.
993 if prev_links
== [] and node
.outputs
[0].is_linked
:
995 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
996 link
, selected_nodes
)]
997 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
998 # To get the placement to look right we need to reverse the order in which we connect the
999 # outputs to the multi input socket.
1000 if i
< len(socket_indices
) - 1:
1001 ind
= socket_indices
[i
]
1002 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
1004 outputs_for_multi_input
.insert(0, node
.outputs
[0])
1005 if outputs_for_multi_input
!= []:
1006 ind
= socket_indices
[-1]
1007 for output
in outputs_for_multi_input
:
1008 connect_sockets(output
, new_node
.inputs
[ind
])
1009 if prev_links
!= []:
1010 for link
in prev_links
:
1011 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
1014 def execute(self
, context
):
1015 settings
= context
.preferences
.addons
[__package__
].preferences
1016 merge_hide
= settings
.merge_hide
1017 merge_position
= settings
.merge_position
# 'center' or 'bottom'
1020 do_hide_shader
= False
1021 if merge_hide
== 'ALWAYS':
1023 do_hide_shader
= True
1024 elif merge_hide
== 'NON_SHADER':
1027 tree_type
= context
.space_data
.node_tree
.type
1028 if tree_type
== 'GEOMETRY':
1029 node_type
= 'GeometryNode'
1030 if tree_type
== 'COMPOSITING':
1031 node_type
= 'CompositorNode'
1032 elif tree_type
== 'SHADER':
1033 node_type
= 'ShaderNode'
1034 elif tree_type
== 'TEXTURE':
1035 node_type
= 'TextureNode'
1036 nodes
, links
= get_nodes_links(context
)
1038 merge_type
= self
.merge_type
1039 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1040 # 'ZCOMBINE' works only if mode == 'MIX'
1041 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1042 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
1045 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
1047 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1048 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
1049 node_type
= 'ShaderNode'
1050 selected_mix
= [] # entry = [index, loc]
1051 selected_shader
= [] # entry = [index, loc]
1052 selected_geometry
= [] # entry = [index, loc]
1053 selected_math
= [] # entry = [index, loc]
1054 selected_vector
= [] # entry = [index, loc]
1055 selected_z
= [] # entry = [index, loc]
1056 selected_alphaover
= [] # entry = [index, loc]
1058 for i
, node
in enumerate(nodes
):
1059 if node
.select
and node
.outputs
:
1060 if merge_type
== 'AUTO':
1061 for (type, types_list
, dst
) in (
1062 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1063 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1064 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
1065 ('VALUE', [t
[0] for t
in operations
], selected_math
),
1066 ('VECTOR', [], selected_vector
),
1068 output
= get_first_enabled_output(node
)
1069 output_type
= output
.type
1070 valid_mode
= mode
in types_list
1071 # When mode is 'MIX' we have to cheat since the mix node is not used in
1073 if tree_type
== 'GEOMETRY':
1075 if output_type
== 'VALUE' and type == 'VALUE':
1077 elif output_type
== 'VECTOR' and type == 'VECTOR':
1079 elif type == 'GEOMETRY':
1081 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1082 # Cheat that output type is 'RGBA',
1083 # and that 'MIX' exists in math operations list.
1084 # This way when selected_mix list is analyzed:
1085 # Node data will be appended even though it doesn't meet requirements.
1086 elif output_type
!= 'SHADER' and mode
== 'MIX':
1087 output_type
= 'RGBA'
1089 if output_type
== type and valid_mode
:
1090 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1092 for (type, types_list
, dst
) in (
1093 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1094 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1095 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1096 ('MATH', [t
[0] for t
in operations
], selected_math
),
1097 ('ZCOMBINE', ('MIX', ), selected_z
),
1098 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1100 if merge_type
== type and mode
in types_list
:
1101 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1102 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1103 # use only 'Mix' nodes for merging.
1104 # For that we add selected_math list to selected_mix list and clear selected_math.
1105 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1106 selected_mix
+= selected_math
1115 selected_alphaover
]:
1118 count_before
= len(nodes
)
1119 # sort list by loc_x - reversed
1120 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1122 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1123 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1125 # Change the node type for math nodes in a geometry node tree.
1126 if tree_type
== 'GEOMETRY':
1127 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1128 node_type
= 'ShaderNode'
1132 node_type
= 'GeometryNode'
1133 if merge_position
== 'CENTER':
1134 # average yloc of last two nodes (lowest two)
1135 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1136 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1142 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1146 if nodes_list
== selected_shader
and not do_hide_shader
:
1148 the_range
= len(nodes_list
) - 1
1149 if len(nodes_list
) == 1:
1152 for i
in range(the_range
):
1153 if nodes_list
== selected_mix
:
1155 if tree_type
== 'COMPOSITING':
1157 add_type
= node_type
+ mix_name
1158 add
= nodes
.new(add_type
)
1159 if tree_type
!= 'COMPOSITING':
1160 add
.data_type
= 'RGBA'
1161 add
.blend_type
= mode
1163 add
.inputs
[0].default_value
= 1.0
1164 add
.show_preview
= False
1170 if tree_type
== 'COMPOSITING':
1173 elif nodes_list
== selected_math
:
1174 add_type
= node_type
+ 'Math'
1175 add
= nodes
.new(add_type
)
1176 add
.operation
= mode
1182 elif nodes_list
== selected_shader
:
1184 add_type
= node_type
+ 'MixShader'
1185 add
= nodes
.new(add_type
)
1186 add
.hide
= do_hide_shader
1192 add_type
= node_type
+ 'AddShader'
1193 add
= nodes
.new(add_type
)
1194 add
.hide
= do_hide_shader
1199 elif nodes_list
== selected_geometry
:
1200 if mode
in ('JOIN', 'MIX'):
1201 add_type
= node_type
+ 'JoinGeometry'
1202 add
= self
.merge_with_multi_input(
1203 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1205 add_type
= node_type
+ 'MeshBoolean'
1206 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1207 add
= self
.merge_with_multi_input(
1208 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1209 add
.operation
= mode
1212 elif nodes_list
== selected_vector
:
1213 add_type
= node_type
+ 'VectorMath'
1214 add
= nodes
.new(add_type
)
1215 add
.operation
= mode
1221 elif nodes_list
== selected_z
:
1222 add
= nodes
.new('CompositorNodeZcombine')
1223 add
.show_preview
= False
1229 elif nodes_list
== selected_alphaover
:
1230 add
= nodes
.new('CompositorNodeAlphaOver')
1231 add
.show_preview
= False
1237 add
.location
= loc_x
, loc_y
1241 # This has already been handled separately
1245 count_after
= len(nodes
)
1246 index
= count_after
- 1
1247 first_selected
= nodes
[nodes_list
[0][0]]
1248 # "last" node has been added as first, so its index is count_before.
1249 last_add
= nodes
[count_before
]
1250 # Create list of invalid indexes.
1251 invalid_nodes
= [nodes
[n
[0]]
1252 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1255 # Two nodes were selected and first selected has no output links, second selected has output links.
1256 # Then add links from last add to all links 'to_socket' of out links of second selected.
1257 first_selected_output
= get_first_enabled_output(first_selected
)
1258 if len(nodes_list
) == 2:
1259 if not first_selected_output
.links
:
1260 second_selected
= nodes
[nodes_list
[1][0]]
1261 for ss_link
in get_first_enabled_output(second_selected
).links
:
1262 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1263 # Link only if "to_node" index not in invalid indexes list.
1264 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1265 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1266 # add links from last_add to all links 'to_socket' of out links of first selected.
1267 for fs_link
in first_selected_output
.links
:
1268 # Link only if "to_node" index not in invalid indexes list.
1269 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1270 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1271 # add link from "first" selected and "first" add node
1272 node_to
= nodes
[count_after
- 1]
1273 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1274 if node_to
.type == 'ZCOMBINE':
1275 for fs_out
in first_selected
.outputs
:
1276 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1277 connect_sockets(fs_out
, node_to
.inputs
[1])
1279 # add links between added ADD nodes and between selected and ADD nodes
1280 for i
in range(count_adds
):
1281 if i
< count_adds
- 1:
1282 node_from
= nodes
[index
]
1283 node_to
= nodes
[index
- 1]
1284 node_to_input_i
= first
1285 node_to_z_i
= 1 # if z combine - link z to first z input
1286 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1287 if node_to
.type == 'ZCOMBINE':
1288 for from_out
in node_from
.outputs
:
1289 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1290 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1291 if len(nodes_list
) > 1:
1292 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1293 node_to
= nodes
[index
]
1294 node_to_input_i
= second
1295 node_to_z_i
= 3 # if z combine - link z to second z input
1296 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1297 if node_to
.type == 'ZCOMBINE':
1298 for from_out
in node_from
.outputs
:
1299 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1300 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1302 # set "last" of added nodes as active
1303 nodes
.active
= last_add
1304 for i
, x
, y
, dx
, h
in nodes_list
:
1305 nodes
[i
].select
= False
1310 class NWBatchChangeNodes(Operator
, NWBase
):
1311 bl_idname
= "node.nw_batch_change"
1312 bl_label
= "Batch Change"
1313 bl_description
= "Batch Change Blend Type and Math Operation"
1314 bl_options
= {'REGISTER', 'UNDO'}
1316 blend_type
: EnumProperty(
1318 items
=blend_types
+ navs
,
1320 operation
: EnumProperty(
1322 items
=operations
+ navs
,
1325 def execute(self
, context
):
1326 blend_type
= self
.blend_type
1327 operation
= self
.operation
1328 for node
in context
.selected_nodes
:
1329 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1330 if blend_type
not in [nav
[0] for nav
in navs
]:
1331 node
.blend_type
= blend_type
1333 if blend_type
== 'NEXT':
1334 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1335 # index = blend_types.index(node.blend_type)
1336 if index
== len(blend_types
) - 1:
1337 node
.blend_type
= blend_types
[0][0]
1339 node
.blend_type
= blend_types
[index
+ 1][0]
1341 if blend_type
== 'PREV':
1342 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1344 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1346 node
.blend_type
= blend_types
[index
- 1][0]
1348 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1349 if operation
not in [nav
[0] for nav
in navs
]:
1350 node
.operation
= operation
1352 if operation
== 'NEXT':
1353 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1354 # index = operations.index(node.operation)
1355 if index
== len(operations
) - 1:
1356 node
.operation
= operations
[0][0]
1358 node
.operation
= operations
[index
+ 1][0]
1360 if operation
== 'PREV':
1361 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1362 # index = operations.index(node.operation)
1364 node
.operation
= operations
[len(operations
) - 1][0]
1366 node
.operation
= operations
[index
- 1][0]
1371 class NWChangeMixFactor(Operator
, NWBase
):
1372 bl_idname
= "node.nw_factor"
1373 bl_label
= "Change Factor"
1374 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1375 bl_options
= {'REGISTER', 'UNDO'}
1377 # option: Change factor.
1378 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1379 # Else - change factor by option value.
1380 option
: FloatProperty()
1382 def execute(self
, context
):
1383 nodes
, links
= get_nodes_links(context
)
1384 option
= self
.option
1385 selected
= [] # entry = index
1386 for si
, node
in enumerate(nodes
):
1388 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1392 fac
= nodes
[si
].inputs
[0]
1393 nodes
[si
].hide
= False
1394 if option
in {0.0, 1.0}:
1395 fac
.default_value
= option
1397 fac
.default_value
+= option
1402 class NWCopySettings(Operator
, NWBase
):
1403 bl_idname
= "node.nw_copy_settings"
1404 bl_label
= "Copy Settings"
1405 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1406 bl_options
= {'REGISTER', 'UNDO'}
1409 def poll(cls
, context
):
1411 if nw_check(context
):
1413 context
.active_node
is not None and
1414 context
.active_node
.type != 'FRAME'
1419 def execute(self
, context
):
1420 node_active
= context
.active_node
1421 node_selected
= context
.selected_nodes
1424 if not (len(node_selected
) > 1):
1425 self
.report({'ERROR'}, "2 nodes must be selected at least")
1426 return {'CANCELLED'}
1428 # Check if active node is in the selection
1429 selected_node_names
= [n
.name
for n
in node_selected
]
1430 if node_active
.name
not in selected_node_names
:
1431 self
.report({'ERROR'}, "No active node")
1432 return {'CANCELLED'}
1434 # Get nodes in selection by type
1435 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1437 if not (len(valid_nodes
) > 1) and node_active
:
1438 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1439 return {'CANCELLED'}
1441 if len(valid_nodes
) != len(node_selected
):
1442 # Report nodes that are not valid
1443 valid_node_names
= [n
.name
for n
in valid_nodes
]
1444 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1447 "Ignored {} (not of the same type as {})".format(
1448 ", ".join(not_valid_names
),
1451 # Reference original
1453 # node_selected_names = [n.name for n in node_selected]
1458 # Deselect all nodes
1459 for i
in node_selected
:
1462 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1463 # Run through all other nodes
1464 for node
in valid_nodes
[1:]:
1466 # Check for frame node
1467 parent
= node
.parent
if node
.parent
else None
1468 node_loc
= [node
.location
.x
, node
.location
.y
]
1470 # Select original to duplicate
1473 # Duplicate selected node
1474 bpy
.ops
.node
.duplicate()
1475 new_node
= context
.selected_nodes
[0]
1478 new_node
.select
= False
1480 # Properties to copy
1481 node_tree
= node
.id_data
1482 props_to_copy
= 'bl_idname name location height width'.split(' ')
1486 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1487 for i
in (i
for i
in mappings
if i
.is_linked
):
1489 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1492 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1493 props_to_copy
.pop(0)
1495 for prop
in props_to_copy
:
1496 setattr(new_node
, prop
, props
[prop
])
1498 # Get the node tree to remove the old node
1499 nodes
= node_tree
.nodes
1501 new_node
.name
= props
['name']
1504 new_node
.parent
= parent
1505 new_node
.location
= node_loc
1507 for str_from
, str_to
in reconnections
:
1508 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1510 success_names
.append(new_node
.name
)
1513 node_tree
.nodes
.active
= orig
1516 "Successfully copied attributes from {} to: {}".format(
1518 ", ".join(success_names
)))
1522 class NWCopyLabel(Operator
, NWBase
):
1523 bl_idname
= "node.nw_copy_label"
1524 bl_label
= "Copy Label"
1525 bl_options
= {'REGISTER', 'UNDO'}
1527 option
: EnumProperty(
1529 description
="Source of name of label",
1531 ('FROM_ACTIVE', 'from active', 'from active node',),
1532 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1533 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1537 def execute(self
, context
):
1538 nodes
, links
= get_nodes_links(context
)
1539 option
= self
.option
1540 active
= nodes
.active
1541 if option
== 'FROM_ACTIVE':
1543 src_label
= active
.label
1544 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1545 node
.label
= src_label
1546 elif option
== 'FROM_NODE':
1547 selected
= [n
for n
in nodes
if n
.select
]
1548 for node
in selected
:
1549 for input in node
.inputs
:
1551 src
= input.links
[0].from_node
1552 node
.label
= src
.label
1554 elif option
== 'FROM_SOCKET':
1555 selected
= [n
for n
in nodes
if n
.select
]
1556 for node
in selected
:
1557 for input in node
.inputs
:
1559 src
= input.links
[0].from_socket
1560 node
.label
= src
.name
1566 class NWClearLabel(Operator
, NWBase
):
1567 bl_idname
= "node.nw_clear_label"
1568 bl_label
= "Clear Label"
1569 bl_options
= {'REGISTER', 'UNDO'}
1571 option
: BoolProperty()
1573 def execute(self
, context
):
1574 nodes
, links
= get_nodes_links(context
)
1575 for node
in [n
for n
in nodes
if n
.select
]:
1580 def invoke(self
, context
, event
):
1582 return self
.execute(context
)
1584 return context
.window_manager
.invoke_confirm(self
, event
)
1587 class NWModifyLabels(Operator
, NWBase
):
1588 """Modify Labels of all selected nodes"""
1589 bl_idname
= "node.nw_modify_labels"
1590 bl_label
= "Modify Labels"
1591 bl_options
= {'REGISTER', 'UNDO'}
1593 prepend
: StringProperty(
1594 name
="Add to Beginning"
1596 append
: StringProperty(
1599 replace_from
: StringProperty(
1600 name
="Text to Replace"
1602 replace_to
: StringProperty(
1606 def execute(self
, context
):
1607 nodes
, links
= get_nodes_links(context
)
1608 for node
in [n
for n
in nodes
if n
.select
]:
1609 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1613 def invoke(self
, context
, event
):
1617 return context
.window_manager
.invoke_props_dialog(self
)
1620 class NWAddTextureSetup(Operator
, NWBase
):
1621 bl_idname
= "node.nw_add_texture"
1622 bl_label
= "Texture Setup"
1623 bl_description
= "Add Texture Node Setup to Selected Shaders"
1624 bl_options
= {'REGISTER', 'UNDO'}
1626 add_mapping
: BoolProperty(
1627 name
="Add Mapping Nodes",
1628 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1632 def poll(cls
, context
):
1633 if nw_check(context
):
1634 space
= context
.space_data
1635 if space
.tree_type
== 'ShaderNodeTree':
1639 def execute(self
, context
):
1640 nodes
, links
= get_nodes_links(context
)
1642 texture_types
= get_texture_node_types()
1643 selected_nodes
= [n
for n
in nodes
if n
.select
]
1645 for node
in selected_nodes
:
1650 target_input
= node
.inputs
[0]
1651 for input in node
.inputs
:
1654 if not input.is_linked
:
1655 target_input
= input
1658 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1663 locx
= node
.location
.x
1664 locy
= node
.location
.y
- (input_index
* padding
)
1666 is_texture_node
= node
.rna_type
.identifier
in texture_types
1667 use_environment_texture
= node
.type == 'BACKGROUND'
1669 # Add an image texture before normal shader nodes.
1670 if not is_texture_node
:
1671 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1672 image_texture_node
= nodes
.new(image_texture_type
)
1673 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1674 image_texture_node
.location
= [locx
- x_offset
, locy
]
1675 nodes
.active
= image_texture_node
1676 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1678 # The mapping setup following this will connect to the first input of this image texture.
1679 target_input
= image_texture_node
.inputs
[0]
1683 if is_texture_node
or self
.add_mapping
:
1685 mapping_node
= nodes
.new('ShaderNodeMapping')
1686 x_offset
= x_offset
+ mapping_node
.width
+ padding
1687 mapping_node
.location
= [locx
- x_offset
, locy
]
1688 connect_sockets(mapping_node
.outputs
[0], target_input
)
1690 # Add Texture Coordinates node.
1691 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1692 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1693 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1695 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1696 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1697 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1698 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1703 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1704 bl_idname
= "node.nw_add_textures_for_principled"
1705 bl_label
= "Principled Texture Setup"
1706 bl_description
= "Add Texture Node Setup for Principled BSDF"
1707 bl_options
= {'REGISTER', 'UNDO'}
1709 directory
: StringProperty(
1713 description
='Folder to search in for image files'
1715 files
: CollectionProperty(
1716 type=bpy
.types
.OperatorFileListElement
,
1717 options
={'HIDDEN', 'SKIP_SAVE'}
1720 relative_path
: BoolProperty(
1721 name
='Relative Path',
1722 description
='Set the file path relative to the blend file, when possible',
1731 def draw(self
, context
):
1732 layout
= self
.layout
1733 layout
.alignment
= 'LEFT'
1735 layout
.prop(self
, 'relative_path')
1738 def poll(cls
, context
):
1740 if nw_check(context
):
1741 space
= context
.space_data
1742 if space
.tree_type
== 'ShaderNodeTree':
1746 def execute(self
, context
):
1747 # Check if everything is ok
1748 if not self
.directory
:
1749 self
.report({'INFO'}, 'No Folder Selected')
1750 return {'CANCELLED'}
1751 if not self
.files
[:]:
1752 self
.report({'INFO'}, 'No Files Selected')
1753 return {'CANCELLED'}
1755 nodes
, links
= get_nodes_links(context
)
1756 active_node
= nodes
.active
1757 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1758 self
.report({'INFO'}, 'Select Principled BSDF')
1759 return {'CANCELLED'}
1761 # Filter textures names for texturetypes in filenames
1762 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1763 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1764 normal_abbr
= tags
.normal
.split(' ')
1765 bump_abbr
= tags
.bump
.split(' ')
1766 gloss_abbr
= tags
.gloss
.split(' ')
1767 rough_abbr
= tags
.rough
.split(' ')
1769 ['Displacement', tags
.displacement
.split(' '), None],
1770 ['Base Color', tags
.base_color
.split(' '), None],
1771 ['Metallic', tags
.metallic
.split(' '), None],
1772 ['Specular IOR Level', tags
.specular
.split(' '), None],
1773 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1774 ['Normal', normal_abbr
+ bump_abbr
, None],
1775 ['Transmission Weight', tags
.transmission
.split(' '), None],
1776 ['Emission Color', tags
.emission
.split(' '), None],
1777 ['Alpha', tags
.alpha
.split(' '), None],
1778 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1781 match_files_to_socket_names(self
.files
, socketnames
)
1782 # Remove socketnames without found files
1783 socketnames
= [s
for s
in socketnames
if s
[2]
1784 and path
.exists(self
.directory
+ s
[2])]
1786 self
.report({'INFO'}, 'No matching images found')
1787 print('No matching images found')
1788 return {'CANCELLED'}
1790 # Don't override path earlier as os.path is used to check the absolute path
1791 import_path
= self
.directory
1792 if self
.relative_path
:
1793 if bpy
.data
.filepath
:
1795 import_path
= bpy
.path
.relpath(self
.directory
)
1800 print('\nMatched Textures:')
1805 roughness_node
= None
1806 for i
, sname
in enumerate(socketnames
):
1807 print(i
, sname
[0], sname
[2])
1809 # DISPLACEMENT NODES
1810 if sname
[0] == 'Displacement':
1811 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1812 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1813 disp_texture
.image
= img
1814 disp_texture
.label
= 'Displacement'
1815 if disp_texture
.image
:
1816 disp_texture
.image
.colorspace_settings
.is_data
= True
1818 # Add displacement offset nodes
1819 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1820 # Align the Displacement node under the active Principled BSDF node
1821 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1822 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1824 # TODO Turn on true displacement in the material
1825 # Too complicated for now
1828 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1830 if not output_node
[0].inputs
[2].is_linked
:
1831 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1835 # AMBIENT OCCLUSION TEXTURE
1836 if sname
[0] == 'Ambient Occlusion':
1837 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1838 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1839 ao_texture
.image
= img
1840 ao_texture
.label
= sname
[0]
1841 if ao_texture
.image
:
1842 ao_texture
.image
.colorspace_settings
.is_data
= True
1846 if not active_node
.inputs
[sname
[0]].is_linked
:
1847 # No texture node connected -> add texture node with new image
1848 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1849 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1850 texture_node
.image
= img
1853 if sname
[0] == 'Normal':
1854 # Test if new texture node is normal or bump map
1855 fname_components
= split_into_components(sname
[2])
1856 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1857 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1859 # If Normal add normal node in between
1860 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1861 link
= connect_sockets(normal_node
.inputs
[1], texture_node
.outputs
[0])
1863 # If Bump add bump node in between
1864 normal_node
= nodes
.new(type='ShaderNodeBump')
1865 link
= connect_sockets(normal_node
.inputs
[2], texture_node
.outputs
[0])
1867 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1868 normal_node_texture
= texture_node
1870 elif sname
[0] == 'Roughness':
1871 # Test if glossy or roughness map
1872 fname_components
= split_into_components(sname
[2])
1873 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1874 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1877 # If Roughness nothing to to
1878 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1881 # If Gloss Map add invert node
1882 invert_node
= nodes
.new(type='ShaderNodeInvert')
1883 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1885 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1886 roughness_node
= texture_node
1889 # This is a simple connection Texture --> Input slot
1890 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1892 # Use non-color except for color inputs
1893 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1894 texture_node
.image
.colorspace_settings
.is_data
= True
1897 # If already texture connected. add to node list for alignment
1898 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1900 # This are all connected texture nodes
1901 texture_nodes
.append(texture_node
)
1902 texture_node
.label
= sname
[0]
1905 texture_nodes
.append(disp_texture
)
1908 # We want the ambient occlusion texture to be the top most texture node
1909 texture_nodes
.insert(0, ao_texture
)
1912 for i
, texture_node
in enumerate(texture_nodes
):
1913 offset
= Vector((-550, (i
* -280) + 200))
1914 texture_node
.location
= active_node
.location
+ offset
1917 # Extra alignment if normal node was added
1918 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1921 # Alignment of invert node if glossy map
1922 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1924 # Add texture input + mapping
1925 mapping
= nodes
.new(type='ShaderNodeMapping')
1926 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1927 if len(texture_nodes
) > 1:
1928 # If more than one texture add reroute node in between
1929 reroute
= nodes
.new(type='NodeReroute')
1930 texture_nodes
.append(reroute
)
1931 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1932 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1933 reroute
.location
= tex_coords
+ Vector((-50, -120))
1934 for texture_node
in texture_nodes
:
1935 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1936 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1938 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1940 # Connect texture_coordiantes to mapping node
1941 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1942 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1943 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1945 # Create frame around tex coords and mapping
1946 frame
= nodes
.new(type='NodeFrame')
1947 frame
.label
= 'Mapping'
1948 mapping
.parent
= frame
1949 texture_input
.parent
= frame
1952 # Create frame around texture nodes
1953 frame
= nodes
.new(type='NodeFrame')
1954 frame
.label
= 'Textures'
1955 for tnode
in texture_nodes
:
1956 tnode
.parent
= frame
1960 active_node
.select
= False
1963 force_update(context
)
1967 class NWAddReroutes(Operator
, NWBase
):
1968 """Add Reroute Nodes and link them to outputs of selected nodes"""
1969 bl_idname
= "node.nw_add_reroutes"
1970 bl_label
= "Add Reroutes"
1971 bl_description
= "Add Reroutes to Outputs"
1972 bl_options
= {'REGISTER', 'UNDO'}
1974 option
: EnumProperty(
1977 ('ALL', 'to all', 'Add to all outputs'),
1978 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1979 ('LINKED', 'to linked', 'Add only to linked outputs'),
1983 def execute(self
, context
):
1984 tree_type
= context
.space_data
.node_tree
.type
1985 option
= self
.option
1986 nodes
, links
= get_nodes_links(context
)
1987 # output valid when option is 'all' or when 'loose' output has no links
1989 post_select
= [] # nodes to be selected after execution
1990 # create reroutes and recreate links
1991 for node
in [n
for n
in nodes
if n
.select
]:
1996 # unhide 'REROUTE' nodes to avoid issues with location.y
1997 if node
.type == 'REROUTE':
1999 # Hack needed to calculate real width
2001 bpy
.ops
.node
.select_all(action
='DESELECT')
2002 helper
= nodes
.new('NodeReroute')
2003 helper
.select
= True
2005 # resize node and helper to zero. Then check locations to calculate width
2006 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2007 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2008 # restore node location
2009 node
.location
= x
, y
2012 # only helper is selected now
2013 bpy
.ops
.node
.delete()
2014 x
= node
.location
.x
+ width
+ 20.0
2015 if node
.type != 'REROUTE':
2019 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
2020 for out_i
, output
in enumerate(node
.outputs
):
2021 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
2022 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
2023 if node
.type != 'R_LAYERS':
2025 else: # if 'R_LAYERS' check if output represent used render pass
2026 node_scene
= node
.scene
2027 node_layer
= node
.layer
2028 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2029 if output
.name
== 'Alpha':
2032 # check entries in global 'rl_outputs' variable
2033 for rlo
in rl_outputs
:
2034 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2035 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
2038 valid
= ((option
== 'ALL') or
2039 (option
== 'LOOSE' and not output
.links
) or
2040 (option
== 'LINKED' and output
.links
))
2041 # Add reroutes only if valid, but offset location in all cases.
2043 n
= nodes
.new('NodeReroute')
2045 for link
in output
.links
:
2046 connect_sockets(n
.outputs
[0], link
.to_socket
)
2047 connect_sockets(output
, n
.inputs
[0])
2049 post_select
.append(n
)
2053 # disselect the node so that after execution of script only newly created nodes are selected
2055 # nicer reroutes distribution along y when node.hide
2057 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2058 for reroute
in [r
for r
in nodes
if r
.select
]:
2059 reroute
.location
.y
-= y_translate
2060 for node
in post_select
:
2066 class NWLinkActiveToSelected(Operator
, NWBase
):
2067 """Link active node to selected nodes basing on various criteria"""
2068 bl_idname
= "node.nw_link_active_to_selected"
2069 bl_label
= "Link Active Node to Selected"
2070 bl_options
= {'REGISTER', 'UNDO'}
2072 replace
: BoolProperty()
2073 use_node_name
: BoolProperty()
2074 use_outputs_names
: BoolProperty()
2077 def poll(cls
, context
):
2079 if nw_check(context
):
2080 if context
.active_node
is not None:
2081 if context
.active_node
.select
:
2085 def execute(self
, context
):
2086 nodes
, links
= get_nodes_links(context
)
2087 replace
= self
.replace
2088 use_node_name
= self
.use_node_name
2089 use_outputs_names
= self
.use_outputs_names
2090 active
= nodes
.active
2091 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2092 outputs
= [] # Only usable outputs of active nodes will be stored here.
2093 for out
in active
.outputs
:
2094 if active
.type != 'R_LAYERS':
2097 # 'R_LAYERS' node type needs special handling.
2098 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2099 # Only outputs that represent used passes should be taken into account
2100 # Check if pass represented by output is used.
2101 # global 'rl_outputs' list will be used for that
2102 for rlo
in rl_outputs
:
2103 pass_used
= False # initial value. Will be set to True if pass is used
2104 if out
.name
== 'Alpha':
2105 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2107 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2108 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2109 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2113 doit
= True # Will be changed to False when links successfully added to previous output.
2116 for node
in selected
:
2117 dst_name
= node
.name
# Will be compared with src_name if needed.
2118 # When node has label - use it as dst_name
2120 dst_name
= node
.label
2121 valid
= True # Initial value. Will be changed to False if names don't match.
2122 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2124 # Set src_name to source node name or label
2125 src_name
= active
.name
2127 src_name
= active
.label
2128 elif use_outputs_names
:
2129 src_name
= (out
.name
, )
2130 for rlo
in rl_outputs
:
2131 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2132 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2133 if dst_name
not in src_name
:
2136 for input in node
.inputs
:
2137 if input.type == out
.type or node
.type == 'REROUTE':
2138 if replace
or not input.is_linked
:
2139 connect_sockets(out
, input)
2140 if not use_node_name
and not use_outputs_names
:
2147 class NWAlignNodes(Operator
, NWBase
):
2148 '''Align the selected nodes neatly in a row/column'''
2149 bl_idname
= "node.nw_align_nodes"
2150 bl_label
= "Align Nodes"
2151 bl_options
= {'REGISTER', 'UNDO'}
2152 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2154 def execute(self
, context
):
2155 nodes
, links
= get_nodes_links(context
)
2156 margin
= self
.margin
2160 if node
.select
and node
.type != 'FRAME':
2161 selection
.append(node
)
2163 # If no nodes are selected, align all nodes
2167 elif nodes
.active
in selection
:
2168 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2170 # Check if nodes should be laid out horizontally or vertically
2171 # use dimension to get center of node, not corner
2172 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2173 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2174 x_range
= max(x_locs
) - min(x_locs
)
2175 y_range
= max(y_locs
) - min(y_locs
)
2176 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2177 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2178 horizontal
= x_range
> y_range
2180 # Sort selection by location of node mid-point
2182 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2184 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2188 for node
in selection
:
2189 current_margin
= margin
2190 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2193 node
.location
.x
= current_pos
2194 current_pos
+= current_margin
+ node
.dimensions
.x
2195 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2197 node
.location
.y
= current_pos
2198 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2199 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2201 # If active node is selected, center nodes around it
2202 if active_loc
is not None:
2203 active_loc_diff
= active_loc
- nodes
.active
.location
2204 for node
in selection
:
2205 node
.location
+= active_loc_diff
2206 else: # Position nodes centered around where they used to be
2207 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2208 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2209 new_mid
= (max(locs
) + min(locs
)) / 2
2210 for node
in selection
:
2212 node
.location
.x
+= (mid_x
- new_mid
)
2214 node
.location
.y
+= (mid_y
- new_mid
)
2219 class NWSelectParentChildren(Operator
, NWBase
):
2220 bl_idname
= "node.nw_select_parent_child"
2221 bl_label
= "Select Parent or Children"
2222 bl_options
= {'REGISTER', 'UNDO'}
2224 option
: EnumProperty(
2227 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2228 ('CHILD', 'Select Children', 'Select members of selected frame'),
2232 def execute(self
, context
):
2233 nodes
, links
= get_nodes_links(context
)
2234 option
= self
.option
2235 selected
= [node
for node
in nodes
if node
.select
]
2236 if option
== 'PARENT':
2237 for sel
in selected
:
2240 parent
.select
= True
2241 else: # option == 'CHILD'
2242 for sel
in selected
:
2243 children
= [node
for node
in nodes
if node
.parent
== sel
]
2244 for kid
in children
:
2250 class NWDetachOutputs(Operator
, NWBase
):
2251 """Detach outputs of selected node leaving inputs linked"""
2252 bl_idname
= "node.nw_detach_outputs"
2253 bl_label
= "Detach Outputs"
2254 bl_options
= {'REGISTER', 'UNDO'}
2256 def execute(self
, context
):
2257 nodes
, links
= get_nodes_links(context
)
2258 selected
= context
.selected_nodes
2259 bpy
.ops
.node
.duplicate_move_keep_inputs()
2260 new_nodes
= context
.selected_nodes
2261 bpy
.ops
.node
.select_all(action
="DESELECT")
2262 for node
in selected
:
2264 bpy
.ops
.node
.delete_reconnect()
2265 for new_node
in new_nodes
:
2266 new_node
.select
= True
2267 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2272 class NWLinkToOutputNode(Operator
):
2273 """Link to Composite node or Material Output node"""
2274 bl_idname
= "node.nw_link_out"
2275 bl_label
= "Connect to Output"
2276 bl_options
= {'REGISTER', 'UNDO'}
2279 def poll(cls
, context
):
2281 if nw_check(context
):
2282 if context
.active_node
is not None:
2283 for out
in context
.active_node
.outputs
:
2284 if is_visible_socket(out
):
2289 def execute(self
, context
):
2290 nodes
, links
= get_nodes_links(context
)
2291 active
= nodes
.active
2293 tree_type
= context
.space_data
.tree_type
2294 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2295 'WORLD': 'ShaderNodeOutputWorld',
2296 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2298 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2299 'CompositorNodeTree': 'CompositorNodeComposite',
2300 'TextureNodeTree': 'TextureNodeOutput',
2301 'GeometryNodeTree': 'NodeGroupOutput',
2304 # check whether the node is an output node and,
2305 # if supported, whether it's the active one
2306 if node
.rna_type
.identifier
== output_type \
2307 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2311 else: # No output node exists
2312 bpy
.ops
.node
.select_all(action
="DESELECT")
2313 output_node
= nodes
.new(output_type
)
2314 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2315 output_node
.location
.y
= active
.location
.y
2318 for i
, output
in enumerate(active
.outputs
):
2319 if is_visible_socket(output
):
2322 for i
, output
in enumerate(active
.outputs
):
2323 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2328 if tree_type
== 'ShaderNodeTree':
2329 if active
.outputs
[output_index
].name
== 'Volume':
2331 elif active
.outputs
[output_index
].name
== 'Displacement':
2333 elif tree_type
== 'GeometryNodeTree':
2334 if active
.outputs
[output_index
].type != 'GEOMETRY':
2335 return {'CANCELLED'}
2336 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2338 force_update(context
) # viewport render does not update
2343 class NWMakeLink(Operator
, NWBase
):
2344 """Make a link from one socket to another"""
2345 bl_idname
= 'node.nw_make_link'
2346 bl_label
= 'Make Link'
2347 bl_options
= {'REGISTER', 'UNDO'}
2348 from_socket
: IntProperty()
2349 to_socket
: IntProperty()
2351 def execute(self
, context
):
2352 nodes
, links
= get_nodes_links(context
)
2354 n1
= nodes
[context
.scene
.NWLazySource
]
2355 n2
= nodes
[context
.scene
.NWLazyTarget
]
2357 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2359 force_update(context
)
2364 class NWCallInputsMenu(Operator
, NWBase
):
2365 """Link from this output"""
2366 bl_idname
= 'node.nw_call_inputs_menu'
2367 bl_label
= 'Make Link'
2368 bl_options
= {'REGISTER', 'UNDO'}
2369 from_socket
: IntProperty()
2371 def execute(self
, context
):
2372 nodes
, links
= get_nodes_links(context
)
2374 context
.scene
.NWSourceSocket
= self
.from_socket
2376 n1
= nodes
[context
.scene
.NWLazySource
]
2377 n2
= nodes
[context
.scene
.NWLazyTarget
]
2378 if len(n2
.inputs
) > 1:
2379 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2380 elif len(n2
.inputs
) == 1:
2381 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2385 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2386 """Add an Image Sequence"""
2387 bl_idname
= 'node.nw_add_sequence'
2388 bl_label
= 'Import Image Sequence'
2389 bl_options
= {'REGISTER', 'UNDO'}
2391 directory
: StringProperty(
2394 filename
: StringProperty(
2397 files
: CollectionProperty(
2398 type=bpy
.types
.OperatorFileListElement
,
2399 options
={'HIDDEN', 'SKIP_SAVE'}
2401 relative_path
: BoolProperty(
2402 name
='Relative Path',
2403 description
='Set the file path relative to the blend file, when possible',
2407 def draw(self
, context
):
2408 layout
= self
.layout
2409 layout
.alignment
= 'LEFT'
2411 layout
.prop(self
, 'relative_path')
2413 def execute(self
, context
):
2414 nodes
, links
= get_nodes_links(context
)
2415 directory
= self
.directory
2416 filename
= self
.filename
2418 tree
= context
.space_data
.node_tree
2421 # print ("\nDIR:", directory)
2422 # print ("FN:", filename)
2423 # print ("Fs:", list(f.name for f in files), '\n')
2425 if tree
.type == 'SHADER':
2426 node_type
= "ShaderNodeTexImage"
2427 elif tree
.type == 'COMPOSITING':
2428 node_type
= "CompositorNodeImage"
2430 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2431 return {'CANCELLED'}
2433 if not files
[0].name
and not filename
:
2434 self
.report({'ERROR'}, "No file chosen")
2435 return {'CANCELLED'}
2436 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2437 # User has selected multiple files without an active one, or the active one is non-existent
2438 filename
= files
[0].name
2440 if not path
.exists(directory
+ filename
):
2441 self
.report({'ERROR'}, filename
+ " does not exist!")
2442 return {'CANCELLED'}
2444 without_ext
= '.'.join(filename
.split('.')[:-1])
2446 # if last digit isn't a number, it's not a sequence
2447 if not without_ext
[-1].isdigit():
2448 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2449 return {'CANCELLED'}
2451 extension
= filename
.split('.')[-1]
2452 reverse
= without_ext
[::-1] # reverse string
2455 for char
in reverse
:
2461 without_num
= without_ext
[:count_numbers
* -1]
2463 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2465 num_frames
= len(files
)
2467 nodes_list
= [node
for node
in nodes
]
2469 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2470 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2474 yloc
+= node_mid_pt(node
, 'y')
2475 yloc
= yloc
/ len(nodes
)
2480 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2482 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2484 node
.label
= name_with_hashes
2486 filepath
= directory
+ (without_ext
+ '.' + extension
)
2487 if self
.relative_path
:
2488 if bpy
.data
.filepath
:
2490 filepath
= bpy
.path
.relpath(filepath
)
2494 img
= bpy
.data
.images
.load(filepath
)
2495 img
.source
= 'SEQUENCE'
2496 img
.name
= name_with_hashes
2498 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2499 # separate the number from the file name of the first file
2500 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2501 image_user
.frame_duration
= num_frames
2506 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2507 """Add multiple images at once"""
2508 bl_idname
= 'node.nw_add_multiple_images'
2509 bl_label
= 'Open Selected Images'
2510 bl_options
= {'REGISTER', 'UNDO'}
2511 directory
: StringProperty(
2514 files
: CollectionProperty(
2515 type=bpy
.types
.OperatorFileListElement
,
2516 options
={'HIDDEN', 'SKIP_SAVE'}
2519 def execute(self
, context
):
2520 nodes
, links
= get_nodes_links(context
)
2522 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2524 if context
.space_data
.node_tree
.type == 'SHADER':
2525 node_type
= "ShaderNodeTexImage"
2526 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2527 node_type
= "CompositorNodeImage"
2529 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2530 return {'CANCELLED'}
2533 for f
in self
.files
:
2536 node
= nodes
.new(node_type
)
2537 new_nodes
.append(node
)
2540 node
.location
.x
= xloc
2541 node
.location
.y
= yloc
2544 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2547 # shift new nodes up to center of tree
2548 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2550 if node
in new_nodes
:
2552 node
.location
.y
+= (list_size
/ 2)
2558 class NWViewerFocus(bpy
.types
.Operator
):
2559 """Set the viewer tile center to the mouse position"""
2560 bl_idname
= "node.nw_viewer_focus"
2561 bl_label
= "Viewer Focus"
2563 x
: bpy
.props
.IntProperty()
2564 y
: bpy
.props
.IntProperty()
2567 def poll(cls
, context
):
2568 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
2570 def execute(self
, context
):
2573 def invoke(self
, context
, event
):
2574 render
= context
.scene
.render
2575 space
= context
.space_data
2576 percent
= render
.resolution_percentage
* 0.01
2578 nodes
, links
= get_nodes_links(context
)
2579 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2582 mlocx
= event
.mouse_region_x
2583 mlocy
= event
.mouse_region_y
2584 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2586 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2587 region_x
= context
.region
.width
2588 region_y
= context
.region
.height
2590 region_center_x
= context
.region
.width
/ 2
2591 region_center_y
= context
.region
.height
/ 2
2593 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2594 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2596 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2597 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2599 margin_x
= region_center_x
- backdrop_center_x
2600 margin_y
= region_center_y
- backdrop_center_y
2602 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2603 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2605 for node
in viewers
:
2606 node
.center_x
= abs_mouse_x
2607 node
.center_y
= abs_mouse_y
2609 return {'PASS_THROUGH'}
2611 return self
.execute(context
)
2614 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2615 """Save the current viewer node to an image file"""
2616 bl_idname
= "node.nw_save_viewer"
2617 bl_label
= "Save This Image"
2618 filepath
: StringProperty(subtype
="FILE_PATH")
2619 filename_ext
: EnumProperty(
2621 description
="Choose the file format to save to",
2622 items
=(('.bmp', "BMP", ""),
2623 ('.rgb', 'IRIS', ""),
2624 ('.png', 'PNG', ""),
2625 ('.jpg', 'JPEG', ""),
2626 ('.jp2', 'JPEG2000', ""),
2627 ('.tga', 'TARGA', ""),
2628 ('.cin', 'CINEON', ""),
2629 ('.dpx', 'DPX', ""),
2630 ('.exr', 'OPEN_EXR', ""),
2631 ('.hdr', 'HDR', ""),
2632 ('.tif', 'TIFF', "")),
2637 def poll(cls
, context
):
2639 if nw_check(context
):
2640 if context
.space_data
.tree_type
== 'CompositorNodeTree':
2641 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
2642 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
2646 def execute(self
, context
):
2663 basename
, ext
= path
.splitext(fp
)
2664 old_render_format
= context
.scene
.render
.image_settings
.file_format
2665 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2666 context
.area
.type = "IMAGE_EDITOR"
2667 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2668 context
.area
.spaces
[0].image
.save_render(fp
)
2669 context
.area
.type = "NODE_EDITOR"
2670 context
.scene
.render
.image_settings
.file_format
= old_render_format
2674 class NWResetNodes(bpy
.types
.Operator
):
2675 """Reset Nodes in Selection"""
2676 bl_idname
= "node.nw_reset_nodes"
2677 bl_label
= "Reset Nodes"
2678 bl_options
= {'REGISTER', 'UNDO'}
2681 def poll(cls
, context
):
2682 space
= context
.space_data
2683 return space
.type == 'NODE_EDITOR'
2685 def execute(self
, context
):
2686 node_active
= context
.active_node
2687 node_selected
= context
.selected_nodes
2688 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2690 # Check if one node is selected at least
2691 if not (len(node_selected
) > 0):
2692 self
.report({'ERROR'}, "1 node must be selected at least")
2693 return {'CANCELLED'}
2695 active_node_name
= node_active
.name
if node_active
.select
else None
2696 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2698 # Create output lists
2699 selected_node_names
= [n
.name
for n
in node_selected
]
2702 # Reset all valid children in a frame
2703 node_active_is_frame
= False
2704 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2705 node_tree
= node_active
.id_data
2706 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2708 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2709 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2710 node_active_is_frame
= True
2712 # Check if valid nodes in selection
2713 if not (len(valid_nodes
) > 0):
2714 # Check for frames only
2715 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2716 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2717 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2719 self
.report({'ERROR'}, "No valid node(s) in selection")
2720 return {'CANCELLED'}
2722 # Report nodes that are not valid
2723 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2724 valid_node_names
= [n
.name
for n
in valid_nodes
]
2725 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2726 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2728 # Deselect all nodes
2729 for i
in node_selected
:
2732 # Run through all valid nodes
2733 for node
in valid_nodes
:
2735 parent
= node
.parent
if node
.parent
else None
2736 node_loc
= [node
.location
.x
, node
.location
.y
]
2738 node_tree
= node
.id_data
2739 props_to_copy
= 'bl_idname name location height width'.split(' ')
2742 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2743 for i
in (i
for i
in mappings
if i
.is_linked
):
2745 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2747 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2749 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2750 props_to_copy
.pop(0)
2752 for prop
in props_to_copy
:
2753 setattr(new_node
, prop
, props
[prop
])
2755 nodes
= node_tree
.nodes
2757 new_node
.name
= props
['name']
2760 new_node
.parent
= parent
2761 new_node
.location
= node_loc
2763 for str_from
, str_to
in reconnections
:
2764 connect_sockets(eval(str_from
), eval(str_to
))
2766 new_node
.select
= False
2767 success_names
.append(new_node
.name
)
2769 # Reselect all nodes
2770 if selected_node_names
and node_active_is_frame
is False:
2771 for i
in selected_node_names
:
2772 node_tree
.nodes
[i
].select
= True
2774 if active_node_name
is not None:
2775 node_tree
.nodes
[active_node_name
].select
= True
2776 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2778 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2800 NWAddPrincipledSetup
,
2802 NWLinkActiveToSelected
,
2804 NWSelectParentChildren
,
2810 NWAddMultipleImages
,
2818 from bpy
.utils
import register_class
2824 from bpy
.utils
import unregister_class
2827 unregister_class(cls
)