1 # SPDX-FileCopyrightText: 2013-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 from bpy
.types
import Operator
8 from bpy
.props
import (
17 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
18 from bpy_extras
.node_utils
import connect_sockets
19 from mathutils
import Vector
23 from itertools
import chain
25 from .interface
import NWConnectionListInputs
, NWConnectionListOutputs
27 from .utils
.constants
import blend_types
, geo_combine_operations
, operations
, navs
, get_texture_node_types
, rl_outputs
28 from .utils
.draw
import draw_callback_nodeoutline
29 from .utils
.paths
import match_files_to_socket_names
, split_into_components
30 from .utils
.nodes
import (node_mid_pt
, autolink
, node_at_pos
, get_nodes_links
, is_viewer_socket
, is_viewer_link
,
31 get_group_output_node
, get_output_location
, force_update
, get_internal_socket
, nw_check
,
32 nw_check_not_empty
, nw_check_selected
, nw_check_active
, nw_check_space_type
,
33 nw_check_node_type
, nw_check_visible_outputs
, nw_check_viewer_node
, NWBase
,
34 get_first_enabled_output
, is_visible_socket
, viewer_socket_name
)
36 class NWLazyMix(Operator
, NWBase
):
37 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
38 bl_idname
= "node.nw_lazy_mix"
39 bl_label
= "Mix Nodes"
40 bl_options
= {'REGISTER', 'UNDO'}
43 def poll(cls
, context
):
44 return nw_check(cls
, context
) and nw_check_not_empty(cls
, context
)
46 def modal(self
, context
, event
):
47 context
.area
.tag_redraw()
48 nodes
, links
= get_nodes_links(context
)
51 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
54 if not context
.scene
.NWBusyDrawing
:
55 node1
= node_at_pos(nodes
, context
, event
)
57 context
.scene
.NWBusyDrawing
= node1
.name
59 if context
.scene
.NWBusyDrawing
!= 'STOP':
60 node1
= nodes
[context
.scene
.NWBusyDrawing
]
62 context
.scene
.NWLazySource
= node1
.name
63 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
65 if event
.type == 'MOUSEMOVE':
66 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
68 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
69 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
70 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
73 node2
= node_at_pos(nodes
, context
, event
)
75 context
.scene
.NWBusyDrawing
= node2
.name
87 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
89 context
.scene
.NWBusyDrawing
= ""
92 elif event
.type == 'ESC':
94 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
97 return {'RUNNING_MODAL'}
99 def invoke(self
, context
, event
):
100 if context
.area
.type == 'NODE_EDITOR':
101 # the arguments we pass the the callback
102 args
= (self
, context
, 'MIX')
103 # Add the region OpenGL drawing callback
104 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
105 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
106 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
110 context
.window_manager
.modal_handler_add(self
)
111 return {'RUNNING_MODAL'}
113 self
.report({'WARNING'}, "View3D not found, cannot run operator")
117 class NWLazyConnect(Operator
, NWBase
):
118 """Connect two nodes without clicking a specific socket (automatically determined"""
119 bl_idname
= "node.nw_lazy_connect"
120 bl_label
= "Lazy Connect"
121 bl_options
= {'REGISTER', 'UNDO'}
122 with_menu
: BoolProperty()
125 def poll(cls
, context
):
126 return nw_check(cls
, context
) and nw_check_not_empty(cls
, context
)
128 def modal(self
, context
, event
):
129 context
.area
.tag_redraw()
130 nodes
, links
= get_nodes_links(context
)
133 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
136 if not context
.scene
.NWBusyDrawing
:
137 node1
= node_at_pos(nodes
, context
, event
)
139 context
.scene
.NWBusyDrawing
= node1
.name
141 if context
.scene
.NWBusyDrawing
!= 'STOP':
142 node1
= nodes
[context
.scene
.NWBusyDrawing
]
144 context
.scene
.NWLazySource
= node1
.name
145 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
147 if event
.type == 'MOUSEMOVE':
148 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
150 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
151 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
152 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
155 node2
= node_at_pos(nodes
, context
, event
)
157 context
.scene
.NWBusyDrawing
= node2
.name
170 original_sel
.append(node
)
172 original_unsel
.append(node
)
176 # link_success = autolink(node1, node2, links)
178 if len(node1
.outputs
) > 1 and node2
.inputs
:
179 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
180 elif len(node1
.outputs
) == 1:
181 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
183 link_success
= autolink(node1
, node2
, links
)
185 for node
in original_sel
:
187 for node
in original_unsel
:
191 force_update(context
)
192 context
.scene
.NWBusyDrawing
= ""
195 elif event
.type == 'ESC':
196 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
199 return {'RUNNING_MODAL'}
201 def invoke(self
, context
, event
):
202 if context
.area
.type == 'NODE_EDITOR':
203 nodes
, links
= get_nodes_links(context
)
204 node
= node_at_pos(nodes
, context
, event
)
206 context
.scene
.NWBusyDrawing
= node
.name
208 # the arguments we pass the the callback
212 args
= (self
, context
, mode
)
213 # Add the region OpenGL drawing callback
214 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
215 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
216 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
220 context
.window_manager
.modal_handler_add(self
)
221 return {'RUNNING_MODAL'}
223 self
.report({'WARNING'}, "View3D not found, cannot run operator")
227 class NWDeleteUnused(Operator
, NWBase
):
228 """Delete all nodes whose output is not used"""
229 bl_idname
= 'node.nw_del_unused'
230 bl_label
= 'Delete Unused Nodes'
231 bl_options
= {'REGISTER', 'UNDO'}
233 delete_muted
: BoolProperty(
235 description
="Delete (but reconnect, like Ctrl-X) all muted nodes",
237 delete_frames
: BoolProperty(
238 name
="Delete Empty Frames",
239 description
="Delete all frames that have no nodes inside them",
242 def is_unused_node(self
, node
):
243 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
244 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
245 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
246 if node
.type in end_types
:
249 for output
in node
.outputs
:
255 def poll(cls
, context
):
256 """Disabled for custom nodes as we do not know which nodes are supported."""
257 return (nw_check(cls
, context
)
258 and nw_check_not_empty(cls
, context
)
259 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
260 'TextureNodeTree', 'GeometryNodeTree'}))
262 def execute(self
, context
):
263 nodes
, links
= get_nodes_links(context
)
269 selection
.append(node
.name
)
275 temp_deleted_nodes
= []
276 del_unused_iterations
= len(nodes
)
277 for it
in range(0, del_unused_iterations
):
278 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
280 if self
.is_unused_node(node
):
282 deleted_nodes
.append(node
.name
)
283 bpy
.ops
.node
.delete()
285 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
288 if self
.delete_frames
:
296 frames_in_use
.append(node
.parent
)
298 if node
.type == 'FRAME' and node
not in frames_in_use
:
301 repeat
= True # repeat for nested frames
303 if node
not in frames_in_use
:
305 deleted_nodes
.append(node
.name
)
306 bpy
.ops
.node
.delete()
308 if self
.delete_muted
:
312 deleted_nodes
.append(node
.name
)
313 bpy
.ops
.node
.delete_reconnect()
315 # get unique list of deleted nodes (iterations would count the same node more than once)
316 deleted_nodes
= list(set(deleted_nodes
))
317 for n
in deleted_nodes
:
318 self
.report({'INFO'}, "Node " + n
+ " deleted")
319 num_deleted
= len(deleted_nodes
)
324 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
326 self
.report({'INFO'}, "Nothing deleted")
329 nodes
, links
= get_nodes_links(context
)
331 if node
.name
in selection
:
335 def invoke(self
, context
, event
):
336 return context
.window_manager
.invoke_confirm(self
, event
)
339 class NWSwapLinks(Operator
, NWBase
):
340 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
341 bl_idname
= 'node.nw_swap_links'
342 bl_label
= 'Swap Links'
343 bl_options
= {'REGISTER', 'UNDO'}
346 def poll(cls
, context
):
347 return nw_check(cls
, context
) and nw_check_selected(cls
, context
, max=2)
349 def execute(self
, context
):
350 nodes
, links
= get_nodes_links(context
)
351 selected_nodes
= context
.selected_nodes
352 n1
= selected_nodes
[0]
355 if len(selected_nodes
) == 2:
356 n2
= selected_nodes
[1]
357 if n1
.outputs
and n2
.outputs
:
362 for output
in n1
.outputs
:
364 for link
in output
.links
:
365 n1_outputs
.append([out_index
, link
.to_socket
])
370 for output
in n2
.outputs
:
372 for link
in output
.links
:
373 n2_outputs
.append([out_index
, link
.to_socket
])
377 for connection
in n1_outputs
:
379 connect_sockets(n2
.outputs
[connection
[0]], connection
[1])
381 self
.report({'WARNING'},
382 "Some connections have been lost due to differing numbers of output sockets")
383 for connection
in n2_outputs
:
385 connect_sockets(n1
.outputs
[connection
[0]], connection
[1])
387 self
.report({'WARNING'},
388 "Some connections have been lost due to differing numbers of output sockets")
390 if n1
.outputs
or n2
.outputs
:
391 self
.report({'WARNING'}, "One of the nodes has no outputs!")
393 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
396 elif len(selected_nodes
) == 1:
397 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
398 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
404 if i1
.is_linked
and not i1
.is_multi_input
:
407 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
409 types
.append([i1
, similar_types
, i
])
411 types
.sort(key
=lambda k
: k
[1], reverse
=True)
417 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
419 i1f
= pair
[0].links
[0].from_socket
420 i1t
= pair
[0].links
[0].to_socket
421 i2f
= pair
[1].links
[0].from_socket
422 i2t
= pair
[1].links
[0].to_socket
423 connect_sockets(i1f
, i2t
)
424 connect_sockets(i2f
, i1t
)
427 fs
= t
[0].links
[0].from_socket
429 links
.remove(t
[0].links
[0])
430 if i
+ 1 == len(n1
.inputs
):
433 while n1
.inputs
[i
].is_linked
:
435 connect_sockets(fs
, n1
.inputs
[i
])
436 elif len(types
) == 2:
437 i1f
= types
[0][0].links
[0].from_socket
438 i1t
= types
[0][0].links
[0].to_socket
439 i2f
= types
[1][0].links
[0].from_socket
440 i2t
= types
[1][0].links
[0].to_socket
441 connect_sockets(i1f
, i2t
)
442 connect_sockets(i2f
, i1t
)
445 self
.report({'WARNING'}, "This node has no input connections to swap!")
447 self
.report({'WARNING'}, "This node has no inputs to swap!")
449 force_update(context
)
453 class NWResetBG(Operator
, NWBase
):
454 """Reset the zoom and position of the background image"""
455 bl_idname
= 'node.nw_bg_reset'
456 bl_label
= 'Reset Backdrop'
457 bl_options
= {'REGISTER', 'UNDO'}
460 def poll(cls
, context
):
461 return nw_check(cls
, context
) and nw_check_space_type(cls
, context
, {'CompositorNodeTree'})
463 def execute(self
, context
):
464 context
.space_data
.backdrop_zoom
= 1
465 context
.space_data
.backdrop_offset
[0] = 0
466 context
.space_data
.backdrop_offset
[1] = 0
470 class NWAddAttrNode(Operator
, NWBase
):
471 """Add an Attribute node with this name"""
472 bl_idname
= 'node.nw_add_attr_node'
473 bl_label
= 'Add UV map'
474 bl_options
= {'REGISTER', 'UNDO'}
476 attr_name
: StringProperty()
479 def poll(cls
, context
):
480 return nw_check(cls
, context
) and nw_check_space_type(cls
, context
, {'ShaderNodeTree'})
482 def execute(self
, context
):
483 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
484 nodes
, links
= get_nodes_links(context
)
485 nodes
.active
.attribute_name
= self
.attr_name
489 class NWPreviewNode(Operator
, NWBase
):
490 bl_idname
= "node.nw_preview_node"
491 bl_label
= "Preview Node"
492 bl_description
= "Connect active node to the Node Group output or the Material Output"
493 bl_options
= {'REGISTER', 'UNDO'}
495 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
496 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
497 run_in_geometry_nodes
: BoolProperty(default
=True)
500 self
.shader_output_type
= ""
501 self
.shader_output_ident
= ""
504 def poll(cls
, context
):
505 """Already implemented natively for compositing nodes."""
506 return (nw_check(cls
, context
) and nw_check_not_empty(cls
, context
)
507 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'GeometryNodeTree'}))
510 def get_output_sockets(node_tree
):
511 return [item
for item
in node_tree
.interface
.items_tree
512 if item
.item_type
== 'SOCKET' and item
.in_out
in {'OUTPUT', 'BOTH'}]
514 def init_shader_variables(self
, space
, shader_type
):
515 if shader_type
== 'OBJECT':
516 if space
.id in bpy
.data
.lights
.values():
517 self
.shader_output_type
= "OUTPUT_LIGHT"
518 self
.shader_output_ident
= "ShaderNodeOutputLight"
520 self
.shader_output_type
= "OUTPUT_MATERIAL"
521 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
523 elif shader_type
== 'WORLD':
524 self
.shader_output_type
= "OUTPUT_WORLD"
525 self
.shader_output_ident
= "ShaderNodeOutputWorld"
527 def ensure_viewer_socket(self
, node_tree
, socket_type
, connect_socket
=None):
528 """Check if a viewer output already exists in a node group, otherwise create it"""
530 output_sockets
= self
.get_output_sockets(node_tree
)
531 if len(output_sockets
):
532 for i
, socket
in enumerate(output_sockets
):
533 if is_viewer_socket(socket
) and socket
.socket_type
== socket_type
:
534 # If viewer output is already used but leads to the same socket we can still use it
535 is_used
= self
.has_socket_other_users(socket
)
537 if connect_socket
is None:
539 groupout
= get_group_output_node(node_tree
)
540 groupout_input
= groupout
.inputs
[i
]
541 links
= groupout_input
.links
542 if connect_socket
not in [link
.from_socket
for link
in links
]:
544 viewer_socket
= socket
547 if viewer_socket
is None:
548 # Create viewer socket
549 viewer_socket
= node_tree
.interface
.new_socket(
550 viewer_socket_name
, in_out
='OUTPUT', socket_type
=socket_type
)
551 viewer_socket
.NWViewerSocket
= True
555 def ensure_group_output(node_tree
):
556 """Check if a group output node exists, otherwise create it"""
557 groupout
= get_group_output_node(node_tree
)
559 groupout
= node_tree
.nodes
.new('NodeGroupOutput')
560 loc_x
, loc_y
= get_output_location(node_tree
)
561 groupout
.location
.x
= loc_x
562 groupout
.location
.y
= loc_y
563 groupout
.select
= False
564 # So that we don't keep on adding new group outputs
565 groupout
.is_active_output
= True
569 def search_sockets(cls
, node
, sockets
, index
=None):
570 """Recursively scan nodes for viewer sockets and store them in a list"""
571 for i
, input_socket
in enumerate(node
.inputs
):
572 if index
and i
!= index
:
574 if len(input_socket
.links
):
575 link
= input_socket
.links
[0]
576 next_node
= link
.from_node
577 external_socket
= link
.from_socket
578 if hasattr(next_node
, "node_tree"):
579 for socket_index
, socket
in enumerate(next_node
.node_tree
.interface
.items_tree
):
580 if socket
.identifier
== external_socket
.identifier
:
582 if is_viewer_socket(socket
) and socket
not in sockets
:
583 sockets
.append(socket
)
584 # continue search inside of node group but restrict socket to where we came from
585 groupout
= get_group_output_node(next_node
.node_tree
)
586 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
589 def scan_nodes(cls
, tree
, sockets
):
590 """Recursively get all viewer sockets in a material tree"""
591 for node
in tree
.nodes
:
592 if hasattr(node
, "node_tree"):
593 if node
.node_tree
is None:
595 for socket
in cls
.get_output_sockets(node
.node_tree
):
596 if is_viewer_socket(socket
) and (socket
not in sockets
):
597 sockets
.append(socket
)
598 cls
.scan_nodes(node
.node_tree
, sockets
)
601 def remove_socket(tree
, socket
):
602 interface
= tree
.interface
603 interface
.remove(socket
)
604 interface
.active_index
= min(interface
.active_index
, len(interface
.items_tree
) - 1)
606 def link_leads_to_used_socket(self
, link
):
607 """Return True if link leads to a socket that is already used in this node"""
608 socket
= get_internal_socket(link
.to_socket
)
609 return socket
and self
.is_socket_used_active_tree(socket
)
611 def is_socket_used_active_tree(self
, socket
):
612 """Ensure used sockets in active node tree is calculated and check given socket"""
613 if not hasattr(self
, "used_viewer_sockets_active_mat"):
614 self
.used_viewer_sockets_active_mat
= []
616 node_tree
= bpy
.context
.space_data
.node_tree
618 if node_tree
.type == 'GEOMETRY':
619 output_node
= get_group_output_node(node_tree
)
620 elif node_tree
.type == 'SHADER':
621 output_node
= get_group_output_node(node_tree
,
622 output_node_type
=self
.shader_output_type
)
624 if output_node
is not None:
625 self
.search_sockets(output_node
, self
.used_viewer_sockets_active_mat
)
626 return socket
in self
.used_viewer_sockets_active_mat
628 def has_socket_other_users(self
, socket
):
629 """List the other users for this socket (other materials or GN groups)"""
630 if not hasattr(self
, "other_viewer_sockets_users"):
631 self
.other_viewer_sockets_users
= []
632 if socket
.socket_type
== 'NodeSocketShader':
633 for mat
in bpy
.data
.materials
:
634 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
637 output_node
= get_group_output_node(mat
.node_tree
,
638 output_node_type
=self
.shader_output_type
)
639 if output_node
is not None:
640 self
.search_sockets(output_node
, self
.other_viewer_sockets_users
)
641 elif socket
.socket_type
== 'NodeSocketGeometry':
642 for obj
in bpy
.data
.objects
:
643 for mod
in obj
.modifiers
:
644 if mod
.type != 'NODES' or mod
.node_group
== bpy
.context
.space_data
.node_tree
:
647 output_node
= get_group_output_node(mod
.node_group
)
648 if output_node
is not None:
649 self
.search_sockets(output_node
, self
.other_viewer_sockets_users
)
650 return socket
in self
.other_viewer_sockets_users
652 def get_output_index(self
, node
, output_node
, is_base_node_tree
, socket_type
, check_type
=False):
653 """Get the next available output socket in the active node"""
656 for i
, out
in enumerate(node
.outputs
):
657 if is_visible_socket(out
) and (not check_type
or out
.type == socket_type
):
658 valid_outputs
.append(i
)
660 out_i
= valid_outputs
[0] # Start index of node's outputs
661 for i
, valid_i
in enumerate(valid_outputs
):
662 for out_link
in node
.outputs
[valid_i
].links
:
663 if is_viewer_link(out_link
, output_node
):
664 if is_base_node_tree
or self
.link_leads_to_used_socket(out_link
):
665 if i
< len(valid_outputs
) - 1:
666 out_i
= valid_outputs
[i
+ 1]
668 out_i
= valid_outputs
[0]
671 def create_links(self
, path
, node
, active_node_socket_id
, socket_type
):
672 """Create links at each step in the node group path."""
673 path
= list(reversed(path
))
674 # Starting from the level of the active node
675 for path_index
, path_element
in enumerate(path
[:-1]):
676 # Ensure there is a viewer node and it has an input
677 tree
= path_element
.node_tree
678 viewer_socket
= self
.ensure_viewer_socket(
680 connect_socket
= node
.outputs
[active_node_socket_id
]
681 if path_index
== 0 else None)
682 if viewer_socket
in self
.delete_sockets
:
683 self
.delete_sockets
.remove(viewer_socket
)
685 # Connect the current to its viewer
686 link_start
= node
.outputs
[active_node_socket_id
]
687 link_end
= self
.ensure_group_output(tree
).inputs
[viewer_socket
.identifier
]
688 connect_sockets(link_start
, link_end
)
690 # Go up in the node group hierarchy
691 next_tree
= path
[path_index
+ 1].node_tree
692 node
= next(n
for n
in next_tree
.nodes
694 and n
.node_tree
== tree
)
696 active_node_socket_id
= viewer_socket
.identifier
697 return node
.outputs
[active_node_socket_id
]
701 for socket
in self
.delete_sockets
:
702 if not self
.has_socket_other_users(socket
):
703 tree
= socket
.id_data
704 self
.remove_socket(tree
, socket
)
706 def invoke(self
, context
, event
):
707 space
= context
.space_data
708 # Ignore operator when running in wrong context.
709 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
710 return {'PASS_THROUGH'}
712 mlocx
= event
.mouse_region_x
713 mlocy
= event
.mouse_region_y
714 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
715 if 'FINISHED' not in select_node
: # only run if mouse click is on a node
718 base_node_tree
= space
.node_tree
719 active_tree
= context
.space_data
.edit_tree
720 path
= context
.space_data
.path
721 nodes
= active_tree
.nodes
722 active
= nodes
.active
724 if not active
and not any(is_visible_socket(out
) for out
in active
.outputs
):
727 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
728 self
.delete_sockets
= []
729 self
.scan_nodes(base_node_tree
, self
.delete_sockets
)
731 if not active
.outputs
:
735 # For geometry node trees, we just connect to the group output
736 if space
.tree_type
== "GeometryNodeTree":
737 socket_type
= 'NodeSocketGeometry'
739 # Find (or create if needed) the output of this node tree
740 output_node
= self
.ensure_group_output(base_node_tree
)
742 active_node_socket_index
= self
.get_output_index(
743 active
, output_node
, base_node_tree
== active_tree
, 'GEOMETRY', check_type
=True
745 # If there is no 'GEOMETRY' output type - We can't preview the node
746 if active_node_socket_index
is None:
749 # Find an input socket of the output of type geometry
750 output_node_socket_index
= None
751 for i
, inp
in enumerate(output_node
.inputs
):
752 if inp
.type == 'GEOMETRY':
753 output_node_socket_index
= i
755 if output_node_socket_index
is None:
756 # Create geometry socket
757 geometry_out_socket
= base_node_tree
.interface
.new_socket(
758 'Geometry', in_out
='OUTPUT', socket_type
=socket_type
760 output_node_socket_index
= geometry_out_socket
.index
762 # For shader node trees, we connect to a material output
763 elif space
.tree_type
== "ShaderNodeTree":
764 socket_type
= 'NodeSocketShader'
765 self
.init_shader_variables(space
, space
.shader_type
)
767 # Get or create material_output node
768 output_node
= get_group_output_node(base_node_tree
,
769 output_node_type
=self
.shader_output_type
)
771 output_node
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
772 output_node
.location
= get_output_location(base_node_tree
)
773 output_node
.select
= False
775 active_node_socket_index
= self
.get_output_index(
776 active
, output_node
, base_node_tree
== active_tree
, 'SHADER'
779 # Cancel if no socket was found. This can happen for group input
780 # nodes with only a virtual socket output.
781 if active_node_socket_index
is None:
784 if active
.outputs
[active_node_socket_index
].name
== "Volume":
785 output_node_socket_index
= 1
787 output_node_socket_index
= 0
789 # If there are no nested node groups, the link starts at the active node
790 node_output
= active
.outputs
[active_node_socket_index
]
792 # Recursively connect inside nested node groups and get the one from base level
793 node_output
= self
.create_links(path
, active
, active_node_socket_index
, socket_type
)
794 output_node_input
= output_node
.inputs
[output_node_socket_index
]
796 # Connect at base level
797 connect_sockets(node_output
, output_node_input
)
800 nodes
.active
= active
802 force_update(context
)
806 class NWFrameSelected(Operator
, NWBase
):
807 bl_idname
= "node.nw_frame_selected"
808 bl_label
= "Frame Selected"
809 bl_description
= "Add a frame node and parent the selected nodes to it"
810 bl_options
= {'REGISTER', 'UNDO'}
812 label_prop
: StringProperty(
814 description
='The visual name of the frame node',
817 use_custom_color_prop
: BoolProperty(
819 description
="Use custom color for the frame node",
822 color_prop
: FloatVectorProperty(
824 description
="The color of the frame node",
825 default
=(0.604, 0.604, 0.604),
826 min=0, max=1, step
=1, precision
=3,
827 subtype
='COLOR_GAMMA', size
=3
830 def draw(self
, context
):
832 layout
.prop(self
, 'label_prop')
833 layout
.prop(self
, 'use_custom_color_prop')
834 col
= layout
.column()
835 col
.active
= self
.use_custom_color_prop
836 col
.prop(self
, 'color_prop', text
="")
838 def execute(self
, context
):
839 nodes
, links
= get_nodes_links(context
)
843 selected
.append(node
)
845 bpy
.ops
.node
.add_node(type='NodeFrame')
847 frm
.label
= self
.label_prop
848 frm
.use_custom_color
= self
.use_custom_color_prop
849 frm
.color
= self
.color_prop
851 for node
in selected
:
857 class NWReloadImages(Operator
):
858 bl_idname
= "node.nw_reload_images"
859 bl_label
= "Reload Images"
860 bl_description
= "Update all the image nodes to match their files on disk"
863 def poll(cls
, context
):
864 """Disabled for custom nodes."""
865 return (nw_check(cls
, context
)
866 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
867 'TextureNodeTree', 'GeometryNodeTree'}))
869 def execute(self
, context
):
870 nodes
, links
= get_nodes_links(context
)
871 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
874 if node
.type in image_types
:
875 if node
.type == "TEXTURE":
876 if node
.texture
: # node has texture assigned
877 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
878 if node
.texture
.image
: # texture has image assigned
879 node
.texture
.image
.reload()
887 self
.report({'INFO'}, "Reloaded images")
888 print("Reloaded " + str(num_reloaded
) + " images")
889 force_update(context
)
892 self
.report({'WARNING'}, "No images found to reload in this node tree")
896 class NWMergeNodes(Operator
, NWBase
):
897 bl_idname
= "node.nw_merge_nodes"
898 bl_label
= "Merge Nodes"
899 bl_description
= "Merge Selected Nodes"
900 bl_options
= {'REGISTER', 'UNDO'}
904 description
="All possible blend types, boolean operations and math operations",
905 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
],
907 merge_type
: EnumProperty(
909 description
="Type of Merge to be used",
911 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
912 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
913 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
914 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
915 ('MATH', 'Math Node', 'Merge using Math Nodes'),
916 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
917 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
921 # Check if the link connects to a node that is in selected_nodes
922 # If not, then check recursively for each link in the nodes outputs.
923 # If yes, return True. If the recursion stops without finding a node
924 # in selected_nodes, it returns False. The depth is used to prevent
925 # getting stuck in a loop because of an already present cycle.
927 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
929 # We're stuck in a cycle, but that cycle was already present,
930 # so we return False.
931 # NOTE: The number 255 is arbitrary, but seems to work well.
934 if node
in selected_nodes
:
938 for output
in node
.outputs
:
940 for olink
in output
.links
:
941 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
943 # None of the outputs found a node in selected_nodes, so there is no cycle.
946 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
947 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
948 # be connected. The last one is assumed to be a multi input socket.
949 # For convenience the node is returned.
951 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
952 # The y-location of the last node
953 loc_y
= nodes_list
[-1][2]
954 if merge_position
== 'CENTER':
955 # Average the y-location
956 for i
in range(len(nodes_list
) - 1):
957 loc_y
+= nodes_list
[i
][2]
958 loc_y
= loc_y
/ len(nodes_list
)
959 new_node
= nodes
.new(node_name
)
960 new_node
.hide
= do_hide
961 new_node
.location
.x
= loc_x
962 new_node
.location
.y
= loc_y
963 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
965 outputs_for_multi_input
= []
966 for i
, node
in enumerate(selected_nodes
):
968 # Search for the first node which had output links that do not create
969 # a cycle, which we can then reconnect afterwards.
970 if prev_links
== [] and node
.outputs
[0].is_linked
:
972 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
973 link
, selected_nodes
)]
974 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
975 # To get the placement to look right we need to reverse the order in which we connect the
976 # outputs to the multi input socket.
977 if i
< len(socket_indices
) - 1:
978 ind
= socket_indices
[i
]
979 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
981 outputs_for_multi_input
.insert(0, node
.outputs
[0])
982 if outputs_for_multi_input
!= []:
983 ind
= socket_indices
[-1]
984 for output
in outputs_for_multi_input
:
985 connect_sockets(output
, new_node
.inputs
[ind
])
987 for link
in prev_links
:
988 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
992 def poll(cls
, context
):
993 return (nw_check(cls
, context
)
994 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
995 'TextureNodeTree', 'GeometryNodeTree'})
996 and nw_check_selected(cls
, context
))
998 def execute(self
, context
):
999 settings
= context
.preferences
.addons
[__package__
].preferences
1000 merge_hide
= settings
.merge_hide
1001 merge_position
= settings
.merge_position
# 'center' or 'bottom'
1004 do_hide_shader
= False
1005 if merge_hide
== 'ALWAYS':
1007 do_hide_shader
= True
1008 elif merge_hide
== 'NON_SHADER':
1011 tree_type
= context
.space_data
.node_tree
.type
1012 if tree_type
== 'GEOMETRY':
1013 node_type
= 'GeometryNode'
1014 if tree_type
== 'COMPOSITING':
1015 node_type
= 'CompositorNode'
1016 elif tree_type
== 'SHADER':
1017 node_type
= 'ShaderNode'
1018 elif tree_type
== 'TEXTURE':
1019 node_type
= 'TextureNode'
1020 nodes
, links
= get_nodes_links(context
)
1022 merge_type
= self
.merge_type
1023 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1024 # 'ZCOMBINE' works only if mode == 'MIX'
1025 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1026 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
1029 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
1031 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1032 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
1033 node_type
= 'ShaderNode'
1034 selected_mix
= [] # entry = [index, loc]
1035 selected_shader
= [] # entry = [index, loc]
1036 selected_geometry
= [] # entry = [index, loc]
1037 selected_math
= [] # entry = [index, loc]
1038 selected_vector
= [] # entry = [index, loc]
1039 selected_z
= [] # entry = [index, loc]
1040 selected_alphaover
= [] # entry = [index, loc]
1042 for i
, node
in enumerate(nodes
):
1043 if node
.select
and node
.outputs
:
1044 if merge_type
== 'AUTO':
1045 for (type, types_list
, dst
) in (
1046 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1047 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1048 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
1049 ('VALUE', [t
[0] for t
in operations
], selected_math
),
1050 ('VECTOR', [], selected_vector
),
1052 output
= get_first_enabled_output(node
)
1053 output_type
= output
.type
1054 valid_mode
= mode
in types_list
1055 # When mode is 'MIX' we have to cheat since the mix node is not used in
1057 if tree_type
== 'GEOMETRY':
1059 if output_type
== 'VALUE' and type == 'VALUE':
1061 elif output_type
== 'VECTOR' and type == 'VECTOR':
1063 elif type == 'GEOMETRY':
1065 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1066 # Cheat that output type is 'RGBA',
1067 # and that 'MIX' exists in math operations list.
1068 # This way when selected_mix list is analyzed:
1069 # Node data will be appended even though it doesn't meet requirements.
1070 elif output_type
!= 'SHADER' and mode
== 'MIX':
1071 output_type
= 'RGBA'
1073 if output_type
== type and valid_mode
:
1074 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1076 for (type, types_list
, dst
) in (
1077 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1078 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1079 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1080 ('MATH', [t
[0] for t
in operations
], selected_math
),
1081 ('ZCOMBINE', ('MIX', ), selected_z
),
1082 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1084 if merge_type
== type and mode
in types_list
:
1085 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1086 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1087 # use only 'Mix' nodes for merging.
1088 # For that we add selected_math list to selected_mix list and clear selected_math.
1089 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1090 selected_mix
+= selected_math
1093 # If no nodes are selected, do nothing and pass through.
1094 if not (selected_mix
+ selected_shader
+ selected_geometry
+ selected_math
1095 + selected_vector
+ selected_z
+ selected_alphaover
):
1096 return {'PASS_THROUGH'}
1105 selected_alphaover
]:
1108 count_before
= len(nodes
)
1109 # sort list by loc_x - reversed
1110 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1112 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1113 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1115 # Change the node type for math nodes in a geometry node tree.
1116 if tree_type
== 'GEOMETRY':
1117 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1118 node_type
= 'ShaderNode'
1122 node_type
= 'GeometryNode'
1123 if merge_position
== 'CENTER':
1124 # average yloc of last two nodes (lowest two)
1125 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1126 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1132 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1136 if nodes_list
== selected_shader
and not do_hide_shader
:
1138 the_range
= len(nodes_list
) - 1
1139 if len(nodes_list
) == 1:
1142 for i
in range(the_range
):
1143 if nodes_list
== selected_mix
:
1145 if tree_type
== 'COMPOSITING':
1147 add_type
= node_type
+ mix_name
1148 add
= nodes
.new(add_type
)
1149 if tree_type
!= 'COMPOSITING':
1150 add
.data_type
= 'RGBA'
1151 add
.blend_type
= mode
1153 add
.inputs
[0].default_value
= 1.0
1154 add
.show_preview
= False
1160 if tree_type
== 'COMPOSITING':
1163 elif nodes_list
== selected_math
:
1164 add_type
= node_type
+ 'Math'
1165 add
= nodes
.new(add_type
)
1166 add
.operation
= mode
1172 elif nodes_list
== selected_shader
:
1174 add_type
= node_type
+ 'MixShader'
1175 add
= nodes
.new(add_type
)
1176 add
.hide
= do_hide_shader
1182 add_type
= node_type
+ 'AddShader'
1183 add
= nodes
.new(add_type
)
1184 add
.hide
= do_hide_shader
1189 elif nodes_list
== selected_geometry
:
1190 if mode
in ('JOIN', 'MIX'):
1191 add_type
= node_type
+ 'JoinGeometry'
1192 add
= self
.merge_with_multi_input(
1193 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1195 add_type
= node_type
+ 'MeshBoolean'
1196 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1197 add
= self
.merge_with_multi_input(
1198 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1199 add
.operation
= mode
1202 elif nodes_list
== selected_vector
:
1203 add_type
= node_type
+ 'VectorMath'
1204 add
= nodes
.new(add_type
)
1205 add
.operation
= mode
1211 elif nodes_list
== selected_z
:
1212 add
= nodes
.new('CompositorNodeZcombine')
1213 add
.show_preview
= False
1219 elif nodes_list
== selected_alphaover
:
1220 add
= nodes
.new('CompositorNodeAlphaOver')
1221 add
.show_preview
= False
1227 add
.location
= loc_x
, loc_y
1231 # This has already been handled separately
1235 count_after
= len(nodes
)
1236 index
= count_after
- 1
1237 first_selected
= nodes
[nodes_list
[0][0]]
1238 # "last" node has been added as first, so its index is count_before.
1239 last_add
= nodes
[count_before
]
1240 # Create list of invalid indexes.
1241 invalid_nodes
= [nodes
[n
[0]]
1242 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1245 # Two nodes were selected and first selected has no output links, second selected has output links.
1246 # Then add links from last add to all links 'to_socket' of out links of second selected.
1247 first_selected_output
= get_first_enabled_output(first_selected
)
1248 if len(nodes_list
) == 2:
1249 if not first_selected_output
.links
:
1250 second_selected
= nodes
[nodes_list
[1][0]]
1251 for ss_link
in get_first_enabled_output(second_selected
).links
:
1252 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1253 # Link only if "to_node" index not in invalid indexes list.
1254 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1255 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1256 # add links from last_add to all links 'to_socket' of out links of first selected.
1257 for fs_link
in first_selected_output
.links
:
1258 # Link only if "to_node" index not in invalid indexes list.
1259 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1260 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1261 # add link from "first" selected and "first" add node
1262 node_to
= nodes
[count_after
- 1]
1263 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1264 if node_to
.type == 'ZCOMBINE':
1265 for fs_out
in first_selected
.outputs
:
1266 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1267 connect_sockets(fs_out
, node_to
.inputs
[1])
1269 # add links between added ADD nodes and between selected and ADD nodes
1270 for i
in range(count_adds
):
1271 if i
< count_adds
- 1:
1272 node_from
= nodes
[index
]
1273 node_to
= nodes
[index
- 1]
1274 node_to_input_i
= first
1275 node_to_z_i
= 1 # if z combine - link z to first z input
1276 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1277 if node_to
.type == 'ZCOMBINE':
1278 for from_out
in node_from
.outputs
:
1279 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1280 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1281 if len(nodes_list
) > 1:
1282 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1283 node_to
= nodes
[index
]
1284 node_to_input_i
= second
1285 node_to_z_i
= 3 # if z combine - link z to second 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
])
1292 # set "last" of added nodes as active
1293 nodes
.active
= last_add
1294 for i
, x
, y
, dx
, h
in nodes_list
:
1295 nodes
[i
].select
= False
1300 class NWBatchChangeNodes(Operator
, NWBase
):
1301 bl_idname
= "node.nw_batch_change"
1302 bl_label
= "Batch Change"
1303 bl_description
= "Batch Change Blend Type and Math Operation"
1304 bl_options
= {'REGISTER', 'UNDO'}
1306 blend_type
: EnumProperty(
1308 items
=blend_types
+ navs
,
1310 operation
: EnumProperty(
1312 items
=operations
+ navs
,
1316 def poll(cls
, context
):
1317 return (nw_check(cls
, context
)
1318 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
1319 'TextureNodeTree', 'GeometryNodeTree'})
1320 and nw_check_selected(cls
, context
))
1322 def execute(self
, context
):
1323 blend_type
= self
.blend_type
1324 operation
= self
.operation
1325 for node
in context
.selected_nodes
:
1326 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1327 if blend_type
not in [nav
[0] for nav
in navs
]:
1328 node
.blend_type
= blend_type
1330 if blend_type
== 'NEXT':
1331 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1332 # index = blend_types.index(node.blend_type)
1333 if index
== len(blend_types
) - 1:
1334 node
.blend_type
= blend_types
[0][0]
1336 node
.blend_type
= blend_types
[index
+ 1][0]
1338 if blend_type
== 'PREV':
1339 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1341 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1343 node
.blend_type
= blend_types
[index
- 1][0]
1345 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1346 if operation
not in [nav
[0] for nav
in navs
]:
1347 node
.operation
= operation
1349 if operation
== 'NEXT':
1350 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1351 # index = operations.index(node.operation)
1352 if index
== len(operations
) - 1:
1353 node
.operation
= operations
[0][0]
1355 node
.operation
= operations
[index
+ 1][0]
1357 if operation
== 'PREV':
1358 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1359 # index = operations.index(node.operation)
1361 node
.operation
= operations
[len(operations
) - 1][0]
1363 node
.operation
= operations
[index
- 1][0]
1368 class NWChangeMixFactor(Operator
, NWBase
):
1369 bl_idname
= "node.nw_factor"
1370 bl_label
= "Change Factor"
1371 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1372 bl_options
= {'REGISTER', 'UNDO'}
1375 def poll(cls
, context
):
1376 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
1378 # option: Change factor.
1379 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1380 # Else - change factor by option value.
1381 option
: FloatProperty()
1383 def execute(self
, context
):
1384 nodes
, links
= get_nodes_links(context
)
1385 option
= self
.option
1386 selected
= [] # entry = index
1387 for si
, node
in enumerate(nodes
):
1389 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1393 fac
= nodes
[si
].inputs
[0]
1394 nodes
[si
].hide
= False
1395 if option
in {0.0, 1.0}:
1396 fac
.default_value
= option
1398 fac
.default_value
+= option
1403 class NWCopySettings(Operator
, NWBase
):
1404 bl_idname
= "node.nw_copy_settings"
1405 bl_label
= "Copy Settings"
1406 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1407 bl_options
= {'REGISTER', 'UNDO'}
1410 def poll(cls
, context
):
1411 return (nw_check(cls
, context
)
1412 and nw_check_active(cls
, context
)
1413 and nw_check_selected(cls
, context
, min=2)
1414 and nw_check_node_type(cls
, context
, 'FRAME', invert
=True))
1416 def execute(self
, context
):
1417 node_active
= context
.active_node
1418 node_selected
= context
.selected_nodes
1419 selected_node_names
= [n
.name
for n
in node_selected
]
1421 # Get nodes in selection by type
1422 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1424 if not (len(valid_nodes
) > 1) and node_active
:
1425 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1426 return {'CANCELLED'}
1428 if len(valid_nodes
) != len(node_selected
):
1429 # Report nodes that are not valid
1430 valid_node_names
= [n
.name
for n
in valid_nodes
]
1431 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1434 "Ignored {} (not of the same type as {})".format(
1435 ", ".join(not_valid_names
),
1438 # Reference original
1440 # node_selected_names = [n.name for n in node_selected]
1445 # Deselect all nodes
1446 for i
in node_selected
:
1449 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1450 # Run through all other nodes
1451 for node
in valid_nodes
[1:]:
1453 # Check for frame node
1454 parent
= node
.parent
if node
.parent
else None
1455 node_loc
= [node
.location
.x
, node
.location
.y
]
1457 # Select original to duplicate
1460 # Duplicate selected node
1461 bpy
.ops
.node
.duplicate()
1462 new_node
= context
.selected_nodes
[0]
1465 new_node
.select
= False
1467 # Properties to copy
1468 node_tree
= node
.id_data
1469 props_to_copy
= 'bl_idname name location height width'.split(' ')
1473 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1474 for i
in (i
for i
in mappings
if i
.is_linked
):
1476 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1479 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1480 props_to_copy
.pop(0)
1482 for prop
in props_to_copy
:
1483 setattr(new_node
, prop
, props
[prop
])
1485 # Get the node tree to remove the old node
1486 nodes
= node_tree
.nodes
1488 new_node
.name
= props
['name']
1491 new_node
.parent
= parent
1492 new_node
.location
= node_loc
1494 for str_from
, str_to
in reconnections
:
1495 connect_sockets(eval(str_from
), eval(str_to
))
1497 success_names
.append(new_node
.name
)
1500 node_tree
.nodes
.active
= orig
1503 "Successfully copied attributes from {} to: {}".format(
1505 ", ".join(success_names
)))
1509 class NWCopyLabel(Operator
, NWBase
):
1510 bl_idname
= "node.nw_copy_label"
1511 bl_label
= "Copy Label"
1512 bl_options
= {'REGISTER', 'UNDO'}
1513 bl_description
= "Copy label from active to selected nodes"
1515 option
: EnumProperty(
1517 description
="Source of name of label",
1519 ('FROM_ACTIVE', 'from active', 'from active node',),
1520 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1521 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1526 def poll(cls
, context
):
1527 return nw_check(cls
, context
) and nw_check_selected(cls
, context
, min=2)
1529 def execute(self
, context
):
1530 nodes
, links
= get_nodes_links(context
)
1531 option
= self
.option
1532 active
= nodes
.active
1533 if option
== 'FROM_ACTIVE':
1535 src_label
= active
.label
1536 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1537 node
.label
= src_label
1538 elif option
== 'FROM_NODE':
1539 selected
= [n
for n
in nodes
if n
.select
]
1540 for node
in selected
:
1541 for input in node
.inputs
:
1543 src
= input.links
[0].from_node
1544 node
.label
= src
.label
1546 elif option
== 'FROM_SOCKET':
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_socket
1552 node
.label
= src
.name
1558 class NWClearLabel(Operator
, NWBase
):
1559 bl_idname
= "node.nw_clear_label"
1560 bl_label
= "Clear Label"
1561 bl_options
= {'REGISTER', 'UNDO'}
1562 bl_description
= "Clear labels on selected nodes"
1564 option
: BoolProperty()
1567 def poll(cls
, context
):
1568 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
1570 def execute(self
, context
):
1571 nodes
, links
= get_nodes_links(context
)
1572 for node
in [n
for n
in nodes
if n
.select
]:
1577 def invoke(self
, context
, event
):
1579 return self
.execute(context
)
1581 return context
.window_manager
.invoke_confirm(self
, event
)
1584 class NWModifyLabels(Operator
, NWBase
):
1585 """Modify labels of all selected nodes"""
1586 bl_idname
= "node.nw_modify_labels"
1587 bl_label
= "Modify Labels"
1588 bl_options
= {'REGISTER', 'UNDO'}
1590 prepend
: StringProperty(
1591 name
="Add to Beginning"
1593 append
: StringProperty(
1596 replace_from
: StringProperty(
1597 name
="Text to Replace"
1599 replace_to
: StringProperty(
1604 def poll(cls
, context
):
1605 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
1607 def execute(self
, context
):
1608 nodes
, links
= get_nodes_links(context
)
1609 for node
in [n
for n
in nodes
if n
.select
]:
1610 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1614 def invoke(self
, context
, event
):
1618 return context
.window_manager
.invoke_props_dialog(self
)
1621 class NWAddTextureSetup(Operator
, NWBase
):
1622 bl_idname
= "node.nw_add_texture"
1623 bl_label
= "Texture Setup"
1624 bl_description
= "Add Texture Node Setup to Selected Shaders"
1625 bl_options
= {'REGISTER', 'UNDO'}
1627 add_mapping
: BoolProperty(
1628 name
="Add Mapping Nodes",
1629 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1633 def poll(cls
, context
):
1634 return (nw_check(cls
, context
)
1635 and nw_check_space_type(cls
, context
, {'ShaderNodeTree'})
1636 and nw_check_selected(cls
, context
))
1638 def execute(self
, context
):
1639 nodes
, links
= get_nodes_links(context
)
1641 texture_types
= get_texture_node_types()
1642 selected_nodes
= [n
for n
in nodes
if n
.select
]
1644 for node
in selected_nodes
:
1649 target_input
= node
.inputs
[0]
1650 for input in node
.inputs
:
1653 if not input.is_linked
:
1654 target_input
= input
1657 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1662 locx
= node
.location
.x
1663 locy
= node
.location
.y
- (input_index
* padding
)
1665 is_texture_node
= node
.rna_type
.identifier
in texture_types
1666 use_environment_texture
= node
.type == 'BACKGROUND'
1668 # Add an image texture before normal shader nodes.
1669 if not is_texture_node
:
1670 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1671 image_texture_node
= nodes
.new(image_texture_type
)
1672 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1673 image_texture_node
.location
= [locx
- x_offset
, locy
]
1674 nodes
.active
= image_texture_node
1675 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1677 # The mapping setup following this will connect to the first input of this image texture.
1678 target_input
= image_texture_node
.inputs
[0]
1682 if is_texture_node
or self
.add_mapping
:
1684 mapping_node
= nodes
.new('ShaderNodeMapping')
1685 x_offset
= x_offset
+ mapping_node
.width
+ padding
1686 mapping_node
.location
= [locx
- x_offset
, locy
]
1687 connect_sockets(mapping_node
.outputs
[0], target_input
)
1689 # Add Texture Coordinates node.
1690 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1691 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1692 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1694 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1695 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1696 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1697 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1702 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1703 bl_idname
= "node.nw_add_textures_for_principled"
1704 bl_label
= "Principled Texture Setup"
1705 bl_description
= "Add Texture Node Setup for Principled BSDF"
1706 bl_options
= {'REGISTER', 'UNDO'}
1708 directory
: StringProperty(
1712 description
='Folder to search in for image files'
1714 files
: CollectionProperty(
1715 type=bpy
.types
.OperatorFileListElement
,
1716 options
={'HIDDEN', 'SKIP_SAVE'}
1719 relative_path
: BoolProperty(
1720 name
='Relative Path',
1721 description
='Set the file path relative to the blend file, when possible',
1730 def draw(self
, context
):
1731 layout
= self
.layout
1732 layout
.alignment
= 'LEFT'
1734 layout
.prop(self
, 'relative_path')
1737 def poll(cls
, context
):
1738 return (nw_check(cls
, context
)
1739 and nw_check_active(cls
, context
)
1740 and nw_check_space_type(cls
, context
, {'ShaderNodeTree'})
1741 and nw_check_node_type(cls
, context
, 'BSDF_PRINCIPLED'))
1743 def execute(self
, context
):
1744 # Check if everything is ok
1745 if not self
.directory
:
1746 self
.report({'INFO'}, 'No folder selected')
1747 return {'CANCELLED'}
1748 if not self
.files
[:]:
1749 self
.report({'INFO'}, 'No files selected')
1750 return {'CANCELLED'}
1752 nodes
, links
= get_nodes_links(context
)
1753 active_node
= nodes
.active
1755 # Filter textures names for texturetypes in filenames
1756 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1757 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1758 normal_abbr
= tags
.normal
.split(' ')
1759 bump_abbr
= tags
.bump
.split(' ')
1760 gloss_abbr
= tags
.gloss
.split(' ')
1761 rough_abbr
= tags
.rough
.split(' ')
1763 ['Displacement', tags
.displacement
.split(' '), None],
1764 ['Base Color', tags
.base_color
.split(' '), None],
1765 ['Metallic', tags
.metallic
.split(' '), None],
1766 ['Specular IOR Level', tags
.specular
.split(' '), None],
1767 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1768 ['Bump', bump_abbr
, None],
1769 ['Normal', normal_abbr
, None],
1770 ['Transmission Weight', tags
.transmission
.split(' '), None],
1771 ['Emission Color', tags
.emission
.split(' '), None],
1772 ['Alpha', tags
.alpha
.split(' '), None],
1773 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1776 match_files_to_socket_names(self
.files
, socketnames
)
1777 # Remove socketnames without found files
1778 socketnames
= [s
for s
in socketnames
if s
[2]
1779 and path
.exists(self
.directory
+ s
[2])]
1781 self
.report({'INFO'}, 'No matching images found')
1782 print('No matching images found')
1783 return {'CANCELLED'}
1785 # Don't override path earlier as os.path is used to check the absolute path
1786 import_path
= self
.directory
1787 if self
.relative_path
:
1788 if bpy
.data
.filepath
:
1790 import_path
= bpy
.path
.relpath(self
.directory
)
1795 print('\nMatched Textures:')
1800 normal_node_texture
= None
1802 bump_node_texture
= None
1803 roughness_node
= None
1804 for i
, sname
in enumerate(socketnames
):
1805 print(i
, sname
[0], sname
[2])
1807 # DISPLACEMENT NODES
1808 if sname
[0] == 'Displacement':
1809 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1810 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1811 disp_texture
.image
= img
1812 disp_texture
.label
= 'Displacement'
1813 if disp_texture
.image
:
1814 disp_texture
.image
.colorspace_settings
.is_data
= True
1816 # Add displacement offset nodes
1817 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1818 # Align the Displacement node under the active Principled BSDF node
1819 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1820 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1822 # TODO Turn on true displacement in the material
1823 # Too complicated for now
1826 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1828 if not output_node
[0].inputs
[2].is_linked
:
1829 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1834 elif sname
[0] == 'Bump':
1835 # Test if new texture node is bump map
1836 fname_components
= split_into_components(sname
[2])
1837 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1839 # If Bump add bump node in between
1840 bump_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1841 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1842 img
.colorspace_settings
.is_data
= True
1843 bump_node_texture
.image
= img
1844 bump_node_texture
.label
= 'Bump'
1847 bump_node
= nodes
.new(type='ShaderNodeBump')
1848 link
= connect_sockets(bump_node
.inputs
[2], bump_node_texture
.outputs
[0])
1849 link
= connect_sockets(active_node
.inputs
['Normal'], bump_node
.outputs
[0])
1853 elif sname
[0] == 'Normal':
1854 # Test if new texture node is normal map
1855 fname_components
= split_into_components(sname
[2])
1856 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1858 # If Normal add normal node in between
1859 normal_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1860 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1861 img
.colorspace_settings
.is_data
= True
1862 normal_node_texture
.image
= img
1863 normal_node_texture
.label
= 'Normal'
1866 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1867 link
= connect_sockets(normal_node
.inputs
[1], normal_node_texture
.outputs
[0])
1868 # Connect to bump node if it was created before, otherwise to the BSDF
1869 if bump_node
is None:
1870 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1872 link
= connect_sockets(bump_node
.inputs
[sname
[0]], normal_node
.outputs
[sname
[0]])
1875 # AMBIENT OCCLUSION TEXTURE
1876 elif sname
[0] == 'Ambient Occlusion':
1877 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1878 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1879 ao_texture
.image
= img
1880 ao_texture
.label
= sname
[0]
1881 if ao_texture
.image
:
1882 ao_texture
.image
.colorspace_settings
.is_data
= True
1886 if not active_node
.inputs
[sname
[0]].is_linked
:
1887 # No texture node connected -> add texture node with new image
1888 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1889 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1890 texture_node
.image
= img
1892 if sname
[0] == 'Roughness':
1893 # Test if glossy or roughness map
1894 fname_components
= split_into_components(sname
[2])
1895 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1896 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1899 # If Roughness nothing to to
1900 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1903 # If Gloss Map add invert node
1904 invert_node
= nodes
.new(type='ShaderNodeInvert')
1905 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1907 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1908 roughness_node
= texture_node
1911 # This is a simple connection Texture --> Input slot
1912 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1914 # Use non-color except for color inputs
1915 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1916 texture_node
.image
.colorspace_settings
.is_data
= True
1919 # If already texture connected. add to node list for alignment
1920 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1922 # This are all connected texture nodes
1923 texture_nodes
.append(texture_node
)
1924 texture_node
.label
= sname
[0]
1927 texture_nodes
.append(disp_texture
)
1928 if bump_node_texture
:
1929 texture_nodes
.append(bump_node_texture
)
1930 if normal_node_texture
:
1931 texture_nodes
.append(normal_node_texture
)
1934 # We want the ambient occlusion texture to be the top most texture node
1935 texture_nodes
.insert(0, ao_texture
)
1938 for i
, texture_node
in enumerate(texture_nodes
):
1939 offset
= Vector((-550, (i
* -280) + 200))
1940 texture_node
.location
= active_node
.location
+ offset
1943 # Extra alignment if normal node was added
1944 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1947 # Extra alignment if bump node was added
1948 bump_node
.location
= bump_node_texture
.location
+ Vector((300, 0))
1951 # Alignment of invert node if glossy map
1952 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1954 # Add texture input + mapping
1955 mapping
= nodes
.new(type='ShaderNodeMapping')
1956 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1957 if len(texture_nodes
) > 1:
1958 # If more than one texture add reroute node in between
1959 reroute
= nodes
.new(type='NodeReroute')
1960 texture_nodes
.append(reroute
)
1961 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1962 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1963 reroute
.location
= tex_coords
+ Vector((-50, -120))
1964 for texture_node
in texture_nodes
:
1965 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1966 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1968 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1970 # Connect texture_coordinates to mapping node
1971 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1972 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1973 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1975 # Create frame around tex coords and mapping
1976 frame
= nodes
.new(type='NodeFrame')
1977 frame
.label
= 'Mapping'
1978 mapping
.parent
= frame
1979 texture_input
.parent
= frame
1982 # Create frame around texture nodes
1983 frame
= nodes
.new(type='NodeFrame')
1984 frame
.label
= 'Textures'
1985 for tnode
in texture_nodes
:
1986 tnode
.parent
= frame
1990 active_node
.select
= False
1993 force_update(context
)
1997 class NWAddReroutes(Operator
, NWBase
):
1998 """Add Reroute Nodes and link them to outputs of selected nodes"""
1999 bl_idname
= "node.nw_add_reroutes"
2000 bl_label
= "Add Reroutes"
2001 bl_description
= "Add Reroutes to Outputs"
2002 bl_options
= {'REGISTER', 'UNDO'}
2004 option
: EnumProperty(
2007 ('ALL', 'to all', 'Add to all outputs'),
2008 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2009 ('LINKED', 'to linked', 'Add only to linked outputs'),
2014 def poll(cls
, context
):
2015 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
2017 def execute(self
, context
):
2018 nodes
, _links
= get_nodes_links(context
)
2019 post_select
= [] # Nodes to be selected after execution.
2022 # Create reroutes and recreate links.
2023 for node
in [n
for n
in nodes
if n
.select
]:
2024 if not node
.outputs
:
2026 x
= node
.location
.x
+ node
.width
+ 20.0
2028 new_node_reroutes
= []
2030 # Unhide 'REROUTE' nodes to avoid issues with location.y
2031 if node
.type == 'REROUTE':
2036 reroutes_count
= 0 # Will be used when aligning reroutes added to hidden nodes.
2037 for out_i
, output
in enumerate(node
.outputs
):
2038 if output
.is_unavailable
:
2040 if node
.type == 'R_LAYERS' and output
.name
!= 'Alpha':
2041 # If 'R_LAYERS' check if output is used in render pass.
2042 # If output is "Alpha", assume it's used. Not available in passes.
2043 node_scene
= node
.scene
2044 node_layer
= node
.layer
2045 for rlo
in rl_outputs
:
2046 # Check entries in global 'rl_outputs' variable.
2047 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2048 if not getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
):
2050 # Output is valid when option is 'all' or when 'loose' output has no links.
2051 valid
= ((self
.option
== 'ALL') or
2052 (self
.option
== 'LOOSE' and not output
.links
) or
2053 (self
.option
== 'LINKED' and output
.links
))
2055 # Add reroutes only if valid.
2056 n
= nodes
.new('NodeReroute')
2058 for link
in output
.links
:
2059 connect_sockets(n
.outputs
[0], link
.to_socket
)
2060 connect_sockets(output
, n
.inputs
[0])
2062 new_node_reroutes
.append(n
)
2063 post_select
.append(n
)
2064 if valid
or not output
.hide
:
2065 # Offset reroutes for all outputs, except hidden ones.
2069 # Nicer reroutes distribution along y when node.hide.
2071 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2072 for reroute
in new_node_reroutes
:
2073 reroute
.location
.y
-= y_translate
2077 # Select only newly created nodes.
2078 node
.select
= node
in post_select
2080 # No new nodes were created.
2081 return {'CANCELLED'}
2086 class NWLinkActiveToSelected(Operator
, NWBase
):
2087 """Link active node to selected nodes basing on various criteria"""
2088 bl_idname
= "node.nw_link_active_to_selected"
2089 bl_label
= "Link Active Node to Selected"
2090 bl_options
= {'REGISTER', 'UNDO'}
2092 replace
: BoolProperty()
2093 use_node_name
: BoolProperty()
2094 use_outputs_names
: BoolProperty()
2097 def poll(cls
, context
):
2098 return (nw_check(cls
, context
)
2099 and nw_check_active(cls
, context
)
2100 and nw_check_selected(cls
, context
, min=2))
2102 def execute(self
, context
):
2103 nodes
, links
= get_nodes_links(context
)
2104 replace
= self
.replace
2105 use_node_name
= self
.use_node_name
2106 use_outputs_names
= self
.use_outputs_names
2107 active
= nodes
.active
2108 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2109 outputs
= [] # Only usable outputs of active nodes will be stored here.
2110 for out
in active
.outputs
:
2111 if active
.type != 'R_LAYERS':
2114 # 'R_LAYERS' node type needs special handling.
2115 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2116 # Only outputs that represent used passes should be taken into account
2117 # Check if pass represented by output is used.
2118 # global 'rl_outputs' list will be used for that
2119 for rlo
in rl_outputs
:
2120 pass_used
= False # initial value. Will be set to True if pass is used
2121 if out
.name
== 'Alpha':
2122 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2124 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2125 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2126 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2130 doit
= True # Will be changed to False when links successfully added to previous output.
2133 for node
in selected
:
2134 dst_name
= node
.name
# Will be compared with src_name if needed.
2135 # When node has label - use it as dst_name
2137 dst_name
= node
.label
2138 valid
= True # Initial value. Will be changed to False if names don't match.
2139 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2141 # Set src_name to source node name or label
2142 src_name
= active
.name
2144 src_name
= active
.label
2145 elif use_outputs_names
:
2146 src_name
= (out
.name
, )
2147 for rlo
in rl_outputs
:
2148 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2149 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2150 if dst_name
not in src_name
:
2153 for input in node
.inputs
:
2154 if input.type == out
.type or node
.type == 'REROUTE':
2155 if replace
or not input.is_linked
:
2156 connect_sockets(out
, input)
2157 if not use_node_name
and not use_outputs_names
:
2164 class NWAlignNodes(Operator
, NWBase
):
2165 '''Align the selected nodes neatly in a row/column'''
2166 bl_idname
= "node.nw_align_nodes"
2167 bl_label
= "Align Nodes"
2168 bl_options
= {'REGISTER', 'UNDO'}
2169 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2172 def poll(cls
, context
):
2173 return nw_check(cls
, context
) and nw_check_not_empty(cls
, context
)
2175 def execute(self
, context
):
2176 nodes
, links
= get_nodes_links(context
)
2177 margin
= self
.margin
2181 if node
.select
and node
.type != 'FRAME':
2182 selection
.append(node
)
2184 # If no nodes are selected, align all nodes
2188 elif nodes
.active
in selection
:
2189 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2191 # Check if nodes should be laid out horizontally or vertically
2192 # use dimension to get center of node, not corner
2193 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2194 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2195 x_range
= max(x_locs
) - min(x_locs
)
2196 y_range
= max(y_locs
) - min(y_locs
)
2197 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2198 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2199 horizontal
= x_range
> y_range
2201 # Sort selection by location of node mid-point
2203 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2205 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2209 for node
in selection
:
2210 current_margin
= margin
2211 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2214 node
.location
.x
= current_pos
2215 current_pos
+= current_margin
+ node
.dimensions
.x
2216 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2218 node
.location
.y
= current_pos
2219 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2220 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2222 # If active node is selected, center nodes around it
2223 if active_loc
is not None:
2224 active_loc_diff
= active_loc
- nodes
.active
.location
2225 for node
in selection
:
2226 node
.location
+= active_loc_diff
2227 else: # Position nodes centered around where they used to be
2228 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2229 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2230 new_mid
= (max(locs
) + min(locs
)) / 2
2231 for node
in selection
:
2233 node
.location
.x
+= (mid_x
- new_mid
)
2235 node
.location
.y
+= (mid_y
- new_mid
)
2240 class NWSelectParentChildren(Operator
, NWBase
):
2241 bl_idname
= "node.nw_select_parent_child"
2242 bl_label
= "Select Parent or Children"
2243 bl_options
= {'REGISTER', 'UNDO'}
2245 option
: EnumProperty(
2248 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2249 ('CHILD', 'Select Children', 'Select members of selected frame'),
2254 def poll(cls
, context
):
2255 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
2257 def execute(self
, context
):
2258 nodes
, links
= get_nodes_links(context
)
2259 option
= self
.option
2260 selected
= [node
for node
in nodes
if node
.select
]
2261 if option
== 'PARENT':
2262 for sel
in selected
:
2265 parent
.select
= True
2266 else: # option == 'CHILD'
2267 for sel
in selected
:
2268 children
= [node
for node
in nodes
if node
.parent
== sel
]
2269 for kid
in children
:
2275 class NWDetachOutputs(Operator
, NWBase
):
2276 """Detach outputs of selected node leaving inputs linked"""
2277 bl_idname
= "node.nw_detach_outputs"
2278 bl_label
= "Detach Outputs"
2279 bl_options
= {'REGISTER', 'UNDO'}
2282 def poll(cls
, context
):
2283 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
2285 def execute(self
, context
):
2286 nodes
, links
= get_nodes_links(context
)
2287 selected
= context
.selected_nodes
2288 bpy
.ops
.node
.duplicate_move_keep_inputs()
2289 new_nodes
= context
.selected_nodes
2290 bpy
.ops
.node
.select_all(action
="DESELECT")
2291 for node
in selected
:
2293 bpy
.ops
.node
.delete_reconnect()
2294 for new_node
in new_nodes
:
2295 new_node
.select
= True
2296 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2301 class NWLinkToOutputNode(Operator
):
2302 """Link to Composite node or Material Output node"""
2303 bl_idname
= "node.nw_link_out"
2304 bl_label
= "Connect to Output"
2305 bl_options
= {'REGISTER', 'UNDO'}
2308 def poll(cls
, context
):
2309 """Disabled for custom nodes as we do not know which nodes are outputs."""
2310 return (nw_check(cls
, context
)
2311 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
2312 'TextureNodeTree', 'GeometryNodeTree'})
2313 and nw_check_active(cls
, context
)
2314 and nw_check_visible_outputs(cls
, context
))
2316 def execute(self
, context
):
2317 nodes
, links
= get_nodes_links(context
)
2318 active
= nodes
.active
2320 tree_type
= context
.space_data
.tree_type
2321 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2322 'WORLD': 'ShaderNodeOutputWorld',
2323 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2325 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2326 'CompositorNodeTree': 'CompositorNodeComposite',
2327 'TextureNodeTree': 'TextureNodeOutput',
2328 'GeometryNodeTree': 'NodeGroupOutput',
2331 # check whether the node is an output node and,
2332 # if supported, whether it's the active one
2333 if node
.rna_type
.identifier
== output_type \
2334 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2338 else: # No output node exists
2339 bpy
.ops
.node
.select_all(action
="DESELECT")
2340 output_node
= nodes
.new(output_type
)
2341 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2342 output_node
.location
.y
= active
.location
.y
2345 for i
, output
in enumerate(active
.outputs
):
2346 if is_visible_socket(output
):
2349 for i
, output
in enumerate(active
.outputs
):
2350 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2355 if tree_type
== 'ShaderNodeTree':
2356 if active
.outputs
[output_index
].name
== 'Volume':
2358 elif active
.outputs
[output_index
].name
== 'Displacement':
2360 elif tree_type
== 'GeometryNodeTree':
2361 if active
.outputs
[output_index
].type != 'GEOMETRY':
2362 return {'CANCELLED'}
2363 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2365 force_update(context
) # viewport render does not update
2370 class NWMakeLink(Operator
, NWBase
):
2371 """Make a link from one socket to another"""
2372 bl_idname
= 'node.nw_make_link'
2373 bl_label
= 'Make Link'
2374 bl_options
= {'REGISTER', 'UNDO'}
2375 from_socket
: IntProperty()
2376 to_socket
: IntProperty()
2378 def execute(self
, context
):
2379 nodes
, links
= get_nodes_links(context
)
2381 n1
= nodes
[context
.scene
.NWLazySource
]
2382 n2
= nodes
[context
.scene
.NWLazyTarget
]
2384 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2386 force_update(context
)
2391 class NWCallInputsMenu(Operator
, NWBase
):
2392 """Link from this output"""
2393 bl_idname
= 'node.nw_call_inputs_menu'
2394 bl_label
= 'Make Link'
2395 bl_options
= {'REGISTER', 'UNDO'}
2396 from_socket
: IntProperty()
2398 def execute(self
, context
):
2399 nodes
, links
= get_nodes_links(context
)
2401 context
.scene
.NWSourceSocket
= self
.from_socket
2403 n1
= nodes
[context
.scene
.NWLazySource
]
2404 n2
= nodes
[context
.scene
.NWLazyTarget
]
2405 if len(n2
.inputs
) > 1:
2406 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2407 elif len(n2
.inputs
) == 1:
2408 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2412 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2413 """Add an Image Sequence"""
2414 bl_idname
= 'node.nw_add_sequence'
2415 bl_label
= 'Import Image Sequence'
2416 bl_options
= {'REGISTER', 'UNDO'}
2418 directory
: StringProperty(
2421 filename
: StringProperty(
2424 files
: CollectionProperty(
2425 type=bpy
.types
.OperatorFileListElement
,
2426 options
={'HIDDEN', 'SKIP_SAVE'}
2428 relative_path
: BoolProperty(
2429 name
='Relative Path',
2430 description
='Set the file path relative to the blend file, when possible',
2435 def poll(cls
, context
):
2436 return (nw_check(cls
, context
)
2437 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree'}))
2439 def draw(self
, context
):
2440 layout
= self
.layout
2441 layout
.alignment
= 'LEFT'
2443 layout
.prop(self
, 'relative_path')
2445 def execute(self
, context
):
2446 nodes
, links
= get_nodes_links(context
)
2447 directory
= self
.directory
2448 filename
= self
.filename
2450 tree
= context
.space_data
.node_tree
2453 # print ("\nDIR:", directory)
2454 # print ("FN:", filename)
2455 # print ("Fs:", list(f.name for f in files), '\n')
2457 if tree
.type == 'SHADER':
2458 node_type
= "ShaderNodeTexImage"
2459 elif tree
.type == 'COMPOSITING':
2460 node_type
= "CompositorNodeImage"
2462 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2463 return {'CANCELLED'}
2465 if not files
[0].name
and not filename
:
2466 self
.report({'ERROR'}, "No file chosen")
2467 return {'CANCELLED'}
2468 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2469 # User has selected multiple files without an active one, or the active one is non-existent
2470 filename
= files
[0].name
2472 if not path
.exists(directory
+ filename
):
2473 self
.report({'ERROR'}, filename
+ " does not exist!")
2474 return {'CANCELLED'}
2476 without_ext
= '.'.join(filename
.split('.')[:-1])
2478 # if last digit isn't a number, it's not a sequence
2479 if not without_ext
[-1].isdigit():
2480 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2481 return {'CANCELLED'}
2483 extension
= filename
.split('.')[-1]
2484 reverse
= without_ext
[::-1] # reverse string
2487 for char
in reverse
:
2493 without_num
= without_ext
[:count_numbers
* -1]
2495 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2497 num_frames
= len(files
)
2499 nodes_list
= [node
for node
in nodes
]
2501 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2502 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2506 yloc
+= node_mid_pt(node
, 'y')
2507 yloc
= yloc
/ len(nodes
)
2512 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2514 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2516 node
.label
= name_with_hashes
2518 filepath
= directory
+ (without_ext
+ '.' + extension
)
2519 if self
.relative_path
:
2520 if bpy
.data
.filepath
:
2522 filepath
= bpy
.path
.relpath(filepath
)
2526 img
= bpy
.data
.images
.load(filepath
)
2527 img
.source
= 'SEQUENCE'
2528 img
.name
= name_with_hashes
2530 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2531 # separate the number from the file name of the first file
2532 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2533 image_user
.frame_duration
= num_frames
2538 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2539 """Add multiple images at once"""
2540 bl_idname
= 'node.nw_add_multiple_images'
2541 bl_label
= 'Open Selected Images'
2542 bl_options
= {'REGISTER', 'UNDO'}
2543 directory
: StringProperty(
2546 files
: CollectionProperty(
2547 type=bpy
.types
.OperatorFileListElement
,
2548 options
={'HIDDEN', 'SKIP_SAVE'}
2552 def poll(cls
, context
):
2553 return (nw_check(cls
, context
)
2554 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree'}))
2556 def execute(self
, context
):
2557 nodes
, links
= get_nodes_links(context
)
2559 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2561 if context
.space_data
.node_tree
.type == 'SHADER':
2562 node_type
= "ShaderNodeTexImage"
2563 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2564 node_type
= "CompositorNodeImage"
2566 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2567 return {'CANCELLED'}
2570 for f
in self
.files
:
2573 node
= nodes
.new(node_type
)
2574 new_nodes
.append(node
)
2577 node
.location
.x
= xloc
2578 node
.location
.y
= yloc
2581 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2584 # shift new nodes up to center of tree
2585 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2587 if node
in new_nodes
:
2589 node
.location
.y
+= (list_size
/ 2)
2595 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2596 """Save the current viewer node to an image file"""
2597 bl_idname
= "node.nw_save_viewer"
2598 bl_label
= "Save This Image"
2599 filepath
: StringProperty(subtype
="FILE_PATH")
2600 filename_ext
: EnumProperty(
2602 description
="Choose the file format to save to",
2603 items
=(('.bmp', "BMP", ""),
2604 ('.rgb', 'IRIS', ""),
2605 ('.png', 'PNG', ""),
2606 ('.jpg', 'JPEG', ""),
2607 ('.jp2', 'JPEG2000', ""),
2608 ('.tga', 'TARGA', ""),
2609 ('.cin', 'CINEON', ""),
2610 ('.dpx', 'DPX', ""),
2611 ('.exr', 'OPEN_EXR', ""),
2612 ('.hdr', 'HDR', ""),
2613 ('.tif', 'TIFF', "")),
2618 def poll(cls
, context
):
2619 return (nw_check(cls
, context
)
2620 and nw_check_space_type(cls
, context
, {'CompositorNodeTree'})
2621 and nw_check_viewer_node(cls
))
2623 def execute(self
, context
):
2640 basename
, ext
= path
.splitext(fp
)
2641 old_render_format
= context
.scene
.render
.image_settings
.file_format
2642 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2643 context
.area
.type = "IMAGE_EDITOR"
2644 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2645 context
.area
.spaces
[0].image
.save_render(fp
)
2646 context
.area
.type = "NODE_EDITOR"
2647 context
.scene
.render
.image_settings
.file_format
= old_render_format
2651 class NWResetNodes(bpy
.types
.Operator
):
2652 """Reset Nodes in Selection"""
2653 bl_idname
= "node.nw_reset_nodes"
2654 bl_label
= "Reset Nodes"
2655 bl_options
= {'REGISTER', 'UNDO'}
2658 def poll(cls
, context
):
2659 return (nw_check(cls
, context
)
2660 and nw_check_selected(cls
, context
)
2661 and nw_check_active(cls
, context
))
2663 def execute(self
, context
):
2664 node_active
= context
.active_node
2665 node_selected
= context
.selected_nodes
2666 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2668 active_node_name
= node_active
.name
if node_active
.select
else None
2669 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2671 # Create output lists
2672 selected_node_names
= [n
.name
for n
in node_selected
]
2675 # Reset all valid children in a frame
2676 node_active_is_frame
= False
2677 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2678 node_tree
= node_active
.id_data
2679 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2681 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2682 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2683 node_active_is_frame
= True
2685 # Check if valid nodes in selection
2686 if not (len(valid_nodes
) > 0):
2687 # Check for frames only
2688 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2689 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2690 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2692 self
.report({'ERROR'}, "No valid node(s) in selection")
2693 return {'CANCELLED'}
2695 # Report nodes that are not valid
2696 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2697 valid_node_names
= [n
.name
for n
in valid_nodes
]
2698 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2699 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2701 # Deselect all nodes
2702 for i
in node_selected
:
2705 # Run through all valid nodes
2706 for node
in valid_nodes
:
2708 parent
= node
.parent
if node
.parent
else None
2709 node_loc
= [node
.location
.x
, node
.location
.y
]
2711 node_tree
= node
.id_data
2712 props_to_copy
= 'bl_idname name location height width'.split(' ')
2715 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2716 for i
in (i
for i
in mappings
if i
.is_linked
):
2718 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2720 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2722 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2723 props_to_copy
.pop(0)
2725 for prop
in props_to_copy
:
2726 setattr(new_node
, prop
, props
[prop
])
2728 nodes
= node_tree
.nodes
2730 new_node
.name
= props
['name']
2733 new_node
.parent
= parent
2734 new_node
.location
= node_loc
2736 for str_from
, str_to
in reconnections
:
2737 connect_sockets(eval(str_from
), eval(str_to
))
2739 new_node
.select
= False
2740 success_names
.append(new_node
.name
)
2742 # Reselect all nodes
2743 if selected_node_names
and node_active_is_frame
is False:
2744 for i
in selected_node_names
:
2745 node_tree
.nodes
[i
].select
= True
2747 if active_node_name
is not None:
2748 node_tree
.nodes
[active_node_name
].select
= True
2749 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2751 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2773 NWAddPrincipledSetup
,
2775 NWLinkActiveToSelected
,
2777 NWSelectParentChildren
,
2783 NWAddMultipleImages
,
2790 from bpy
.utils
import register_class
2796 from bpy
.utils
import unregister_class
2799 unregister_class(cls
)