1 # SPDX-FileCopyrightText: 2013-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 from bpy
.types
import Operator
8 from bpy
.props
import (
17 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
18 from bpy_extras
.node_utils
import connect_sockets
19 from mathutils
import Vector
23 from itertools
import chain
25 from .interface
import NWConnectionListInputs
, NWConnectionListOutputs
27 from .utils
.constants
import blend_types
, geo_combine_operations
, operations
, navs
, get_texture_node_types
, rl_outputs
28 from .utils
.draw
import draw_callback_nodeoutline
29 from .utils
.paths
import match_files_to_socket_names
, split_into_components
30 from .utils
.nodes
import (node_mid_pt
, autolink
, node_at_pos
, get_active_tree
, get_nodes_links
, is_viewer_socket
,
31 is_viewer_link
, get_group_output_node
, get_output_location
, force_update
, get_internal_socket
,
32 nw_check
, nw_check_space_type
, NWBase
, get_first_enabled_output
, is_visible_socket
,
35 class NWLazyMix(Operator
, NWBase
):
36 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
37 bl_idname
= "node.nw_lazy_mix"
38 bl_label
= "Mix Nodes"
39 bl_options
= {'REGISTER', 'UNDO'}
41 def modal(self
, context
, event
):
42 context
.area
.tag_redraw()
43 nodes
, links
= get_nodes_links(context
)
46 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
49 if not context
.scene
.NWBusyDrawing
:
50 node1
= node_at_pos(nodes
, context
, event
)
52 context
.scene
.NWBusyDrawing
= node1
.name
54 if context
.scene
.NWBusyDrawing
!= 'STOP':
55 node1
= nodes
[context
.scene
.NWBusyDrawing
]
57 context
.scene
.NWLazySource
= node1
.name
58 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
60 if event
.type == 'MOUSEMOVE':
61 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
63 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
64 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
65 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
68 node2
= node_at_pos(nodes
, context
, event
)
70 context
.scene
.NWBusyDrawing
= node2
.name
82 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
84 context
.scene
.NWBusyDrawing
= ""
87 elif event
.type == 'ESC':
89 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
92 return {'RUNNING_MODAL'}
94 def invoke(self
, context
, event
):
95 if context
.area
.type == 'NODE_EDITOR':
96 # the arguments we pass the the callback
97 args
= (self
, context
, 'MIX')
98 # Add the region OpenGL drawing callback
99 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
100 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
101 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
105 context
.window_manager
.modal_handler_add(self
)
106 return {'RUNNING_MODAL'}
108 self
.report({'WARNING'}, "View3D not found, cannot run operator")
112 class NWLazyConnect(Operator
, NWBase
):
113 """Connect two nodes without clicking a specific socket (automatically determined"""
114 bl_idname
= "node.nw_lazy_connect"
115 bl_label
= "Lazy Connect"
116 bl_options
= {'REGISTER', 'UNDO'}
117 with_menu
: BoolProperty()
119 def modal(self
, context
, event
):
120 context
.area
.tag_redraw()
121 nodes
, links
= get_nodes_links(context
)
124 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
127 if not context
.scene
.NWBusyDrawing
:
128 node1
= node_at_pos(nodes
, context
, event
)
130 context
.scene
.NWBusyDrawing
= node1
.name
132 if context
.scene
.NWBusyDrawing
!= 'STOP':
133 node1
= nodes
[context
.scene
.NWBusyDrawing
]
135 context
.scene
.NWLazySource
= node1
.name
136 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
138 if event
.type == 'MOUSEMOVE':
139 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
141 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
142 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
143 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
146 node2
= node_at_pos(nodes
, context
, event
)
148 context
.scene
.NWBusyDrawing
= node2
.name
161 original_sel
.append(node
)
163 original_unsel
.append(node
)
167 # link_success = autolink(node1, node2, links)
169 if len(node1
.outputs
) > 1 and node2
.inputs
:
170 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
171 elif len(node1
.outputs
) == 1:
172 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
174 link_success
= autolink(node1
, node2
, links
)
176 for node
in original_sel
:
178 for node
in original_unsel
:
182 force_update(context
)
183 context
.scene
.NWBusyDrawing
= ""
186 elif event
.type == 'ESC':
187 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
190 return {'RUNNING_MODAL'}
192 def invoke(self
, context
, event
):
193 if context
.area
.type == 'NODE_EDITOR':
194 nodes
, links
= get_nodes_links(context
)
195 node
= node_at_pos(nodes
, context
, event
)
197 context
.scene
.NWBusyDrawing
= node
.name
199 # the arguments we pass the the callback
203 args
= (self
, context
, mode
)
204 # Add the region OpenGL drawing callback
205 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
206 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
207 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
211 context
.window_manager
.modal_handler_add(self
)
212 return {'RUNNING_MODAL'}
214 self
.report({'WARNING'}, "View3D not found, cannot run operator")
218 class NWDeleteUnused(Operator
, NWBase
):
219 """Delete all nodes whose output is not used"""
220 bl_idname
= 'node.nw_del_unused'
221 bl_label
= 'Delete Unused Nodes'
222 bl_options
= {'REGISTER', 'UNDO'}
224 delete_muted
: BoolProperty(
226 description
="Delete (but reconnect, like Ctrl-X) all muted nodes",
228 delete_frames
: BoolProperty(
229 name
="Delete Empty Frames",
230 description
="Delete all frames that have no nodes inside them",
233 def is_unused_node(self
, node
):
234 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
235 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
236 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
237 if node
.type in end_types
:
240 for output
in node
.outputs
:
246 def poll(cls
, context
):
247 """Disabled for custom nodes as we do not know which nodes are supported."""
248 return (nw_check(context
)
249 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
250 'TextureNodeTree', 'GeometryNodeTree')
251 and context
.space_data
.node_tree
.nodes
)
253 def execute(self
, context
):
254 nodes
, links
= get_nodes_links(context
)
260 selection
.append(node
.name
)
266 temp_deleted_nodes
= []
267 del_unused_iterations
= len(nodes
)
268 for it
in range(0, del_unused_iterations
):
269 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
271 if self
.is_unused_node(node
):
273 deleted_nodes
.append(node
.name
)
274 bpy
.ops
.node
.delete()
276 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
279 if self
.delete_frames
:
287 frames_in_use
.append(node
.parent
)
289 if node
.type == 'FRAME' and node
not in frames_in_use
:
292 repeat
= True # repeat for nested frames
294 if node
not in frames_in_use
:
296 deleted_nodes
.append(node
.name
)
297 bpy
.ops
.node
.delete()
299 if self
.delete_muted
:
303 deleted_nodes
.append(node
.name
)
304 bpy
.ops
.node
.delete_reconnect()
306 # get unique list of deleted nodes (iterations would count the same node more than once)
307 deleted_nodes
= list(set(deleted_nodes
))
308 for n
in deleted_nodes
:
309 self
.report({'INFO'}, "Node " + n
+ " deleted")
310 num_deleted
= len(deleted_nodes
)
315 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
317 self
.report({'INFO'}, "Nothing deleted")
320 nodes
, links
= get_nodes_links(context
)
322 if node
.name
in selection
:
326 def invoke(self
, context
, event
):
327 return context
.window_manager
.invoke_confirm(self
, event
)
330 class NWSwapLinks(Operator
, NWBase
):
331 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
332 bl_idname
= 'node.nw_swap_links'
333 bl_label
= 'Swap Links'
334 bl_options
= {'REGISTER', 'UNDO'}
337 def poll(cls
, context
):
338 return nw_check(context
) and context
.selected_nodes
and len(context
.selected_nodes
) <= 2
340 def execute(self
, context
):
341 nodes
, links
= get_nodes_links(context
)
342 selected_nodes
= context
.selected_nodes
343 n1
= selected_nodes
[0]
346 if len(selected_nodes
) == 2:
347 n2
= selected_nodes
[1]
348 if n1
.outputs
and n2
.outputs
:
353 for output
in n1
.outputs
:
355 for link
in output
.links
:
356 n1_outputs
.append([out_index
, link
.to_socket
])
361 for output
in n2
.outputs
:
363 for link
in output
.links
:
364 n2_outputs
.append([out_index
, link
.to_socket
])
368 for connection
in n1_outputs
:
370 connect_sockets(n2
.outputs
[connection
[0]], connection
[1])
372 self
.report({'WARNING'},
373 "Some connections have been lost due to differing numbers of output sockets")
374 for connection
in n2_outputs
:
376 connect_sockets(n1
.outputs
[connection
[0]], connection
[1])
378 self
.report({'WARNING'},
379 "Some connections have been lost due to differing numbers of output sockets")
381 if n1
.outputs
or n2
.outputs
:
382 self
.report({'WARNING'}, "One of the nodes has no outputs!")
384 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
387 elif len(selected_nodes
) == 1:
388 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
389 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
395 if i1
.is_linked
and not i1
.is_multi_input
:
398 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
400 types
.append([i1
, similar_types
, i
])
402 types
.sort(key
=lambda k
: k
[1], reverse
=True)
408 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
410 i1f
= pair
[0].links
[0].from_socket
411 i1t
= pair
[0].links
[0].to_socket
412 i2f
= pair
[1].links
[0].from_socket
413 i2t
= pair
[1].links
[0].to_socket
414 connect_sockets(i1f
, i2t
)
415 connect_sockets(i2f
, i1t
)
418 fs
= t
[0].links
[0].from_socket
420 links
.remove(t
[0].links
[0])
421 if i
+ 1 == len(n1
.inputs
):
424 while n1
.inputs
[i
].is_linked
:
426 connect_sockets(fs
, n1
.inputs
[i
])
427 elif len(types
) == 2:
428 i1f
= types
[0][0].links
[0].from_socket
429 i1t
= types
[0][0].links
[0].to_socket
430 i2f
= types
[1][0].links
[0].from_socket
431 i2t
= types
[1][0].links
[0].to_socket
432 connect_sockets(i1f
, i2t
)
433 connect_sockets(i2f
, i1t
)
436 self
.report({'WARNING'}, "This node has no input connections to swap!")
438 self
.report({'WARNING'}, "This node has no inputs to swap!")
440 force_update(context
)
444 class NWResetBG(Operator
, NWBase
):
445 """Reset the zoom and position of the background image"""
446 bl_idname
= 'node.nw_bg_reset'
447 bl_label
= 'Reset Backdrop'
448 bl_options
= {'REGISTER', 'UNDO'}
451 def poll(cls
, context
):
452 return (nw_check(context
)
453 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
455 def execute(self
, context
):
456 context
.space_data
.backdrop_zoom
= 1
457 context
.space_data
.backdrop_offset
[0] = 0
458 context
.space_data
.backdrop_offset
[1] = 0
462 class NWAddAttrNode(Operator
, NWBase
):
463 """Add an Attribute node with this name"""
464 bl_idname
= 'node.nw_add_attr_node'
465 bl_label
= 'Add UV map'
466 bl_options
= {'REGISTER', 'UNDO'}
468 attr_name
: StringProperty()
471 def poll(cls
, context
):
472 return (nw_check(context
)
473 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
475 def execute(self
, context
):
476 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
477 nodes
, links
= get_nodes_links(context
)
478 nodes
.active
.attribute_name
= self
.attr_name
482 class NWPreviewNode(Operator
, NWBase
):
483 bl_idname
= "node.nw_preview_node"
484 bl_label
= "Preview Node"
485 bl_description
= "Connect active node to the Node Group output or the Material Output"
486 bl_options
= {'REGISTER', 'UNDO'}
488 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
489 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
490 run_in_geometry_nodes
: BoolProperty(default
=True)
493 self
.shader_output_type
= ""
494 self
.shader_output_ident
= ""
497 def poll(cls
, context
):
498 """Already implemented natively for compositing nodes."""
499 return (nw_check(context
)
500 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'GeometryNodeTree')
501 and (not context
.active_node
502 or context
.active_node
.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"}))
505 def get_output_sockets(node_tree
):
506 return [item
for item
in node_tree
.interface
.items_tree
if item
.item_type
== 'SOCKET' and item
.in_out
in {'OUTPUT', 'BOTH'}]
508 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
509 """Check if a viewer output already exists in a node group, otherwise create it"""
510 if not hasattr(node
, "node_tree"):
514 output_sockets
= self
.get_output_sockets(node
.node_tree
)
515 if len(output_sockets
):
516 for i
, socket
in enumerate(output_sockets
):
517 if is_viewer_socket(socket
) and socket
.socket_type
== socket_type
:
518 # If viewer output is already used but leads to the same socket we can still use it
519 is_used
= self
.is_socket_used_other_mats(socket
)
521 if connect_socket
is None:
523 groupout
= get_group_output_node(node
.node_tree
)
524 groupout_input
= groupout
.inputs
[i
]
525 links
= groupout_input
.links
526 if connect_socket
not in [link
.from_socket
for link
in links
]:
528 viewer_socket
= socket
531 if viewer_socket
is None:
532 # Create viewer socket
533 viewer_socket
= node
.node_tree
.interface
.new_socket(viewer_socket_name
, in_out
='OUTPUT', socket_type
=socket_type
)
534 viewer_socket
.NWViewerSocket
= True
537 def init_shader_variables(self
, space
, shader_type
):
538 if shader_type
== 'OBJECT':
539 if space
.id in bpy
.data
.lights
.values():
540 self
.shader_output_type
= "OUTPUT_LIGHT"
541 self
.shader_output_ident
= "ShaderNodeOutputLight"
543 self
.shader_output_type
= "OUTPUT_MATERIAL"
544 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
546 elif shader_type
== 'WORLD':
547 self
.shader_output_type
= "OUTPUT_WORLD"
548 self
.shader_output_ident
= "ShaderNodeOutputWorld"
551 def ensure_group_output(tree
):
552 """Check if a group output node exists, otherwise create it"""
553 groupout
= get_group_output_node(tree
)
555 groupout
= tree
.nodes
.new('NodeGroupOutput')
556 loc_x
, loc_y
= get_output_location(tree
)
557 groupout
.location
.x
= loc_x
558 groupout
.location
.y
= loc_y
559 groupout
.select
= False
560 # So that we don't keep on adding new group outputs
561 groupout
.is_active_output
= True
565 def search_sockets(cls
, node
, sockets
, index
=None):
566 """Recursively scan nodes for viewer sockets and store them in a list"""
567 for i
, input_socket
in enumerate(node
.inputs
):
568 if index
and i
!= index
:
570 if len(input_socket
.links
):
571 link
= input_socket
.links
[0]
572 next_node
= link
.from_node
573 external_socket
= link
.from_socket
574 if hasattr(next_node
, "node_tree"):
575 for socket_index
, socket
in enumerate(next_node
.node_tree
.interface
.items_tree
):
576 if socket
.identifier
== external_socket
.identifier
:
578 if is_viewer_socket(socket
) and socket
not in sockets
:
579 sockets
.append(socket
)
580 # continue search inside of node group but restrict socket to where we came from
581 groupout
= get_group_output_node(next_node
.node_tree
)
582 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
585 def scan_nodes(cls
, tree
, sockets
):
586 """Recursively get all viewer sockets in a material tree"""
587 for node
in tree
.nodes
:
588 if hasattr(node
, "node_tree"):
589 if node
.node_tree
is None:
591 for socket
in cls
.get_output_sockets(node
.node_tree
):
592 if is_viewer_socket(socket
) and (socket
not in sockets
):
593 sockets
.append(socket
)
594 cls
.scan_nodes(node
.node_tree
, sockets
)
597 def remove_socket(tree
, socket
):
598 interface
= tree
.interface
599 interface
.remove(socket
)
600 interface
.active_index
= min(interface
.active_index
, len(interface
.items_tree
) - 1)
602 def link_leads_to_used_socket(self
, link
):
603 """Return True if link leads to a socket that is already used in this material"""
604 socket
= get_internal_socket(link
.to_socket
)
605 return (socket
and self
.is_socket_used_active_mat(socket
))
607 def is_socket_used_active_mat(self
, socket
):
608 """Ensure used sockets in active material is calculated and check given socket"""
609 if not hasattr(self
, "used_viewer_sockets_active_mat"):
610 self
.used_viewer_sockets_active_mat
= []
611 output_node
= get_group_output_node(bpy
.context
.space_data
.node_tree
,
612 output_node_type
=self
.shader_output_type
)
614 if output_node
is not None:
615 self
.search_sockets(output_node
, self
.used_viewer_sockets_active_mat
)
616 return socket
in self
.used_viewer_sockets_active_mat
618 def is_socket_used_other_mats(self
, socket
):
619 """Ensure used sockets in other materials are calculated and check given socket"""
620 if not hasattr(self
, "used_viewer_sockets_other_mats"):
621 self
.used_viewer_sockets_other_mats
= []
622 for mat
in bpy
.data
.materials
:
623 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
626 output_node
= get_group_output_node(mat
.node_tree
,
627 output_node_type
=self
.shader_output_type
)
628 if output_node
is not None:
629 self
.search_sockets(output_node
, self
.used_viewer_sockets_other_mats
)
630 return socket
in self
.used_viewer_sockets_other_mats
632 def get_output_index(self
, base_node_tree
, nodes
, output_node
, socket_type
, check_type
=False):
633 """Get the next available output socket in the active node"""
636 for i
, out
in enumerate(nodes
.active
.outputs
):
637 if is_visible_socket(out
) and (not check_type
or out
.type == socket_type
):
638 valid_outputs
.append(i
)
640 out_i
= valid_outputs
[0] # Start index of node's outputs
641 for i
, valid_i
in enumerate(valid_outputs
):
642 for out_link
in nodes
.active
.outputs
[valid_i
].links
:
643 if is_viewer_link(out_link
, output_node
):
644 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
645 if i
< len(valid_outputs
) - 1:
646 out_i
= valid_outputs
[i
+ 1]
648 out_i
= valid_outputs
[0]
651 def create_links(self
, tree
, link_end
, active
, out_i
, socket_type
):
652 """Create links through node groups until we reach the active node"""
653 while tree
.nodes
.active
!= active
:
654 node
= tree
.nodes
.active
655 viewer_socket
= self
.ensure_viewer_socket(
656 node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
657 link_start
= node
.outputs
[viewer_socket
.identifier
]
658 if viewer_socket
in self
.delete_sockets
:
659 self
.delete_sockets
.remove(viewer_socket
)
660 connect_sockets(link_start
, link_end
)
662 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[viewer_socket
.identifier
]
663 tree
= tree
.nodes
.active
.node_tree
664 connect_sockets(active
.outputs
[out_i
], link_end
)
666 def invoke(self
, context
, event
):
667 space
= context
.space_data
668 # Ignore operator when running in wrong context.
669 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
670 return {'PASS_THROUGH'}
672 mlocx
= event
.mouse_region_x
673 mlocy
= event
.mouse_region_y
674 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
675 if not 'FINISHED' in select_node
: # only run if mouse click is on a node
678 active_tree
, path_to_tree
= get_active_tree(context
)
679 nodes
, links
= active_tree
.nodes
, active_tree
.links
680 base_node_tree
= space
.node_tree
681 active
= nodes
.active
683 if not active
and not any(is_visible_socket(out
) for out
in active
.outputs
):
686 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
687 self
.delete_sockets
= []
688 self
.scan_nodes(base_node_tree
, self
.delete_sockets
)
690 # For geometry node trees we just connect to the group output
691 if space
.tree_type
== "GeometryNodeTree" and active
.outputs
:
692 socket_type
= 'GEOMETRY'
694 # Find (or create if needed) the output of this node tree
695 output_node
= self
.ensure_group_output(base_node_tree
)
697 out_i
= self
.get_output_index(base_node_tree
, nodes
, output_node
, 'GEOMETRY', check_type
=True)
698 # If there is no 'GEOMETRY' output type - We can't preview the node
702 # Find an input socket of the output of type geometry
703 geometry_out_index
= None
704 for i
, inp
in enumerate(output_node
.inputs
):
705 if inp
.type == socket_type
:
706 geometry_out_index
= i
708 if geometry_out_index
is None:
709 # Create geometry socket
710 geometry_out_socket
= base_node_tree
.interface
.new_socket(
711 'Geometry', in_out
='OUTPUT', socket_type
='NodeSocketGeometry'
713 geometry_out_index
= geometry_out_socket
.index
715 output_socket
= output_node
.inputs
[geometry_out_index
]
717 self
.create_links(base_node_tree
, output_socket
, active
, out_i
, 'NodeSocketGeometry')
719 # What follows is code for the shader editor
720 elif space
.tree_type
== "ShaderNodeTree" and active
.outputs
:
721 shader_type
= space
.shader_type
722 self
.init_shader_variables(space
, shader_type
)
723 socket_type
= 'NodeSocketShader'
725 # Get or create material_output node
726 output_node
= get_group_output_node(base_node_tree
,
727 output_node_type
=self
.shader_output_type
)
729 output_node
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
730 output_node
.location
= get_output_location(base_node_tree
)
731 output_node
.select
= False
733 out_i
= self
.get_output_index(base_node_tree
, nodes
, output_node
, 'SHADER')
735 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
736 output_socket
= output_node
.inputs
[materialout_index
]
738 self
.create_links(base_node_tree
, output_socket
, active
, out_i
, 'NodeSocketShader')
741 for socket
in self
.delete_sockets
:
742 if not self
.is_socket_used_other_mats(socket
):
743 tree
= socket
.id_data
744 self
.remove_socket(tree
, socket
)
746 nodes
.active
= active
748 force_update(context
)
752 class NWFrameSelected(Operator
, NWBase
):
753 bl_idname
= "node.nw_frame_selected"
754 bl_label
= "Frame Selected"
755 bl_description
= "Add a frame node and parent the selected nodes to it"
756 bl_options
= {'REGISTER', 'UNDO'}
758 label_prop
: StringProperty(
760 description
='The visual name of the frame node',
763 use_custom_color_prop
: BoolProperty(
765 description
="Use custom color for the frame node",
768 color_prop
: FloatVectorProperty(
770 description
="The color of the frame node",
771 default
=(0.604, 0.604, 0.604),
772 min=0, max=1, step
=1, precision
=3,
773 subtype
='COLOR_GAMMA', size
=3
776 def draw(self
, context
):
778 layout
.prop(self
, 'label_prop')
779 layout
.prop(self
, 'use_custom_color_prop')
780 col
= layout
.column()
781 col
.active
= self
.use_custom_color_prop
782 col
.prop(self
, 'color_prop', text
="")
784 def execute(self
, context
):
785 nodes
, links
= get_nodes_links(context
)
789 selected
.append(node
)
791 bpy
.ops
.node
.add_node(type='NodeFrame')
793 frm
.label
= self
.label_prop
794 frm
.use_custom_color
= self
.use_custom_color_prop
795 frm
.color
= self
.color_prop
797 for node
in selected
:
803 class NWReloadImages(Operator
):
804 bl_idname
= "node.nw_reload_images"
805 bl_label
= "Reload Images"
806 bl_description
= "Update all the image nodes to match their files on disk"
809 def poll(cls
, context
):
810 return (nw_check(context
)
811 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
812 'TextureNodeTree', 'GeometryNodeTree')
813 and context
.active_node
is not None
814 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
816 def execute(self
, context
):
817 nodes
, links
= get_nodes_links(context
)
818 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
821 if node
.type in image_types
:
822 if node
.type == "TEXTURE":
823 if node
.texture
: # node has texture assigned
824 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
825 if node
.texture
.image
: # texture has image assigned
826 node
.texture
.image
.reload()
834 self
.report({'INFO'}, "Reloaded images")
835 print("Reloaded " + str(num_reloaded
) + " images")
836 force_update(context
)
839 self
.report({'WARNING'}, "No images found to reload in this node tree")
843 class NWMergeNodes(Operator
, NWBase
):
844 bl_idname
= "node.nw_merge_nodes"
845 bl_label
= "Merge Nodes"
846 bl_description
= "Merge Selected Nodes"
847 bl_options
= {'REGISTER', 'UNDO'}
851 description
="All possible blend types, boolean operations and math operations",
852 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
],
854 merge_type
: EnumProperty(
856 description
="Type of Merge to be used",
858 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
859 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
860 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
861 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
862 ('MATH', 'Math Node', 'Merge using Math Nodes'),
863 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
864 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
868 # Check if the link connects to a node that is in selected_nodes
869 # If not, then check recursively for each link in the nodes outputs.
870 # If yes, return True. If the recursion stops without finding a node
871 # in selected_nodes, it returns False. The depth is used to prevent
872 # getting stuck in a loop because of an already present cycle.
874 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
876 # We're stuck in a cycle, but that cycle was already present,
877 # so we return False.
878 # NOTE: The number 255 is arbitrary, but seems to work well.
881 if node
in selected_nodes
:
885 for output
in node
.outputs
:
887 for olink
in output
.links
:
888 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
890 # None of the outputs found a node in selected_nodes, so there is no cycle.
893 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
894 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
895 # be connected. The last one is assumed to be a multi input socket.
896 # For convenience the node is returned.
898 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
899 # The y-location of the last node
900 loc_y
= nodes_list
[-1][2]
901 if merge_position
== 'CENTER':
902 # Average the y-location
903 for i
in range(len(nodes_list
) - 1):
904 loc_y
+= nodes_list
[i
][2]
905 loc_y
= loc_y
/ len(nodes_list
)
906 new_node
= nodes
.new(node_name
)
907 new_node
.hide
= do_hide
908 new_node
.location
.x
= loc_x
909 new_node
.location
.y
= loc_y
910 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
912 outputs_for_multi_input
= []
913 for i
, node
in enumerate(selected_nodes
):
915 # Search for the first node which had output links that do not create
916 # a cycle, which we can then reconnect afterwards.
917 if prev_links
== [] and node
.outputs
[0].is_linked
:
919 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
920 link
, selected_nodes
)]
921 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
922 # To get the placement to look right we need to reverse the order in which we connect the
923 # outputs to the multi input socket.
924 if i
< len(socket_indices
) - 1:
925 ind
= socket_indices
[i
]
926 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
928 outputs_for_multi_input
.insert(0, node
.outputs
[0])
929 if outputs_for_multi_input
!= []:
930 ind
= socket_indices
[-1]
931 for output
in outputs_for_multi_input
:
932 connect_sockets(output
, new_node
.inputs
[ind
])
934 for link
in prev_links
:
935 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
939 def poll(cls
, context
):
940 return (nw_check(context
)
941 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
942 'TextureNodeTree', 'GeometryNodeTree'))
944 def execute(self
, context
):
945 settings
= context
.preferences
.addons
[__package__
].preferences
946 merge_hide
= settings
.merge_hide
947 merge_position
= settings
.merge_position
# 'center' or 'bottom'
950 do_hide_shader
= False
951 if merge_hide
== 'ALWAYS':
953 do_hide_shader
= True
954 elif merge_hide
== 'NON_SHADER':
957 tree_type
= context
.space_data
.node_tree
.type
958 if tree_type
== 'GEOMETRY':
959 node_type
= 'GeometryNode'
960 if tree_type
== 'COMPOSITING':
961 node_type
= 'CompositorNode'
962 elif tree_type
== 'SHADER':
963 node_type
= 'ShaderNode'
964 elif tree_type
== 'TEXTURE':
965 node_type
= 'TextureNode'
966 nodes
, links
= get_nodes_links(context
)
968 merge_type
= self
.merge_type
969 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
970 # 'ZCOMBINE' works only if mode == 'MIX'
971 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
972 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
975 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
977 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
978 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
979 node_type
= 'ShaderNode'
980 selected_mix
= [] # entry = [index, loc]
981 selected_shader
= [] # entry = [index, loc]
982 selected_geometry
= [] # entry = [index, loc]
983 selected_math
= [] # entry = [index, loc]
984 selected_vector
= [] # entry = [index, loc]
985 selected_z
= [] # entry = [index, loc]
986 selected_alphaover
= [] # entry = [index, loc]
988 for i
, node
in enumerate(nodes
):
989 if node
.select
and node
.outputs
:
990 if merge_type
== 'AUTO':
991 for (type, types_list
, dst
) in (
992 ('SHADER', ('MIX', 'ADD'), selected_shader
),
993 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
994 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
995 ('VALUE', [t
[0] for t
in operations
], selected_math
),
996 ('VECTOR', [], selected_vector
),
998 output
= get_first_enabled_output(node
)
999 output_type
= output
.type
1000 valid_mode
= mode
in types_list
1001 # When mode is 'MIX' we have to cheat since the mix node is not used in
1003 if tree_type
== 'GEOMETRY':
1005 if output_type
== 'VALUE' and type == 'VALUE':
1007 elif output_type
== 'VECTOR' and type == 'VECTOR':
1009 elif type == 'GEOMETRY':
1011 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1012 # Cheat that output type is 'RGBA',
1013 # and that 'MIX' exists in math operations list.
1014 # This way when selected_mix list is analyzed:
1015 # Node data will be appended even though it doesn't meet requirements.
1016 elif output_type
!= 'SHADER' and mode
== 'MIX':
1017 output_type
= 'RGBA'
1019 if output_type
== type and valid_mode
:
1020 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1022 for (type, types_list
, dst
) in (
1023 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1024 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1025 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1026 ('MATH', [t
[0] for t
in operations
], selected_math
),
1027 ('ZCOMBINE', ('MIX', ), selected_z
),
1028 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1030 if merge_type
== type and mode
in types_list
:
1031 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1032 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1033 # use only 'Mix' nodes for merging.
1034 # For that we add selected_math list to selected_mix list and clear selected_math.
1035 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1036 selected_mix
+= selected_math
1045 selected_alphaover
]:
1048 count_before
= len(nodes
)
1049 # sort list by loc_x - reversed
1050 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1052 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1053 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1055 # Change the node type for math nodes in a geometry node tree.
1056 if tree_type
== 'GEOMETRY':
1057 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1058 node_type
= 'ShaderNode'
1062 node_type
= 'GeometryNode'
1063 if merge_position
== 'CENTER':
1064 # average yloc of last two nodes (lowest two)
1065 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1066 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1072 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1076 if nodes_list
== selected_shader
and not do_hide_shader
:
1078 the_range
= len(nodes_list
) - 1
1079 if len(nodes_list
) == 1:
1082 for i
in range(the_range
):
1083 if nodes_list
== selected_mix
:
1085 if tree_type
== 'COMPOSITING':
1087 add_type
= node_type
+ mix_name
1088 add
= nodes
.new(add_type
)
1089 if tree_type
!= 'COMPOSITING':
1090 add
.data_type
= 'RGBA'
1091 add
.blend_type
= mode
1093 add
.inputs
[0].default_value
= 1.0
1094 add
.show_preview
= False
1100 if tree_type
== 'COMPOSITING':
1103 elif nodes_list
== selected_math
:
1104 add_type
= node_type
+ 'Math'
1105 add
= nodes
.new(add_type
)
1106 add
.operation
= mode
1112 elif nodes_list
== selected_shader
:
1114 add_type
= node_type
+ 'MixShader'
1115 add
= nodes
.new(add_type
)
1116 add
.hide
= do_hide_shader
1122 add_type
= node_type
+ 'AddShader'
1123 add
= nodes
.new(add_type
)
1124 add
.hide
= do_hide_shader
1129 elif nodes_list
== selected_geometry
:
1130 if mode
in ('JOIN', 'MIX'):
1131 add_type
= node_type
+ 'JoinGeometry'
1132 add
= self
.merge_with_multi_input(
1133 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1135 add_type
= node_type
+ 'MeshBoolean'
1136 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1137 add
= self
.merge_with_multi_input(
1138 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1139 add
.operation
= mode
1142 elif nodes_list
== selected_vector
:
1143 add_type
= node_type
+ 'VectorMath'
1144 add
= nodes
.new(add_type
)
1145 add
.operation
= mode
1151 elif nodes_list
== selected_z
:
1152 add
= nodes
.new('CompositorNodeZcombine')
1153 add
.show_preview
= False
1159 elif nodes_list
== selected_alphaover
:
1160 add
= nodes
.new('CompositorNodeAlphaOver')
1161 add
.show_preview
= False
1167 add
.location
= loc_x
, loc_y
1171 # This has already been handled separately
1175 count_after
= len(nodes
)
1176 index
= count_after
- 1
1177 first_selected
= nodes
[nodes_list
[0][0]]
1178 # "last" node has been added as first, so its index is count_before.
1179 last_add
= nodes
[count_before
]
1180 # Create list of invalid indexes.
1181 invalid_nodes
= [nodes
[n
[0]]
1182 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1185 # Two nodes were selected and first selected has no output links, second selected has output links.
1186 # Then add links from last add to all links 'to_socket' of out links of second selected.
1187 first_selected_output
= get_first_enabled_output(first_selected
)
1188 if len(nodes_list
) == 2:
1189 if not first_selected_output
.links
:
1190 second_selected
= nodes
[nodes_list
[1][0]]
1191 for ss_link
in get_first_enabled_output(second_selected
).links
:
1192 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1193 # Link only if "to_node" index not in invalid indexes list.
1194 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1195 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1196 # add links from last_add to all links 'to_socket' of out links of first selected.
1197 for fs_link
in first_selected_output
.links
:
1198 # Link only if "to_node" index not in invalid indexes list.
1199 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1200 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1201 # add link from "first" selected and "first" add node
1202 node_to
= nodes
[count_after
- 1]
1203 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1204 if node_to
.type == 'ZCOMBINE':
1205 for fs_out
in first_selected
.outputs
:
1206 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1207 connect_sockets(fs_out
, node_to
.inputs
[1])
1209 # add links between added ADD nodes and between selected and ADD nodes
1210 for i
in range(count_adds
):
1211 if i
< count_adds
- 1:
1212 node_from
= nodes
[index
]
1213 node_to
= nodes
[index
- 1]
1214 node_to_input_i
= first
1215 node_to_z_i
= 1 # if z combine - link z to first z input
1216 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1217 if node_to
.type == 'ZCOMBINE':
1218 for from_out
in node_from
.outputs
:
1219 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1220 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1221 if len(nodes_list
) > 1:
1222 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1223 node_to
= nodes
[index
]
1224 node_to_input_i
= second
1225 node_to_z_i
= 3 # if z combine - link z to second z input
1226 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1227 if node_to
.type == 'ZCOMBINE':
1228 for from_out
in node_from
.outputs
:
1229 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1230 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1232 # set "last" of added nodes as active
1233 nodes
.active
= last_add
1234 for i
, x
, y
, dx
, h
in nodes_list
:
1235 nodes
[i
].select
= False
1240 class NWBatchChangeNodes(Operator
, NWBase
):
1241 bl_idname
= "node.nw_batch_change"
1242 bl_label
= "Batch Change"
1243 bl_description
= "Batch Change Blend Type and Math Operation"
1244 bl_options
= {'REGISTER', 'UNDO'}
1246 blend_type
: EnumProperty(
1248 items
=blend_types
+ navs
,
1250 operation
: EnumProperty(
1252 items
=operations
+ navs
,
1256 def poll(cls
, context
):
1257 return (nw_check(context
)
1258 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
1259 'TextureNodeTree', 'GeometryNodeTree'))
1261 def execute(self
, context
):
1262 blend_type
= self
.blend_type
1263 operation
= self
.operation
1264 for node
in context
.selected_nodes
:
1265 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1266 if blend_type
not in [nav
[0] for nav
in navs
]:
1267 node
.blend_type
= blend_type
1269 if blend_type
== 'NEXT':
1270 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1271 # index = blend_types.index(node.blend_type)
1272 if index
== len(blend_types
) - 1:
1273 node
.blend_type
= blend_types
[0][0]
1275 node
.blend_type
= blend_types
[index
+ 1][0]
1277 if blend_type
== 'PREV':
1278 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1280 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1282 node
.blend_type
= blend_types
[index
- 1][0]
1284 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1285 if operation
not in [nav
[0] for nav
in navs
]:
1286 node
.operation
= operation
1288 if operation
== 'NEXT':
1289 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1290 # index = operations.index(node.operation)
1291 if index
== len(operations
) - 1:
1292 node
.operation
= operations
[0][0]
1294 node
.operation
= operations
[index
+ 1][0]
1296 if operation
== 'PREV':
1297 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1298 # index = operations.index(node.operation)
1300 node
.operation
= operations
[len(operations
) - 1][0]
1302 node
.operation
= operations
[index
- 1][0]
1307 class NWChangeMixFactor(Operator
, NWBase
):
1308 bl_idname
= "node.nw_factor"
1309 bl_label
= "Change Factor"
1310 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1311 bl_options
= {'REGISTER', 'UNDO'}
1313 # option: Change factor.
1314 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1315 # Else - change factor by option value.
1316 option
: FloatProperty()
1318 def execute(self
, context
):
1319 nodes
, links
= get_nodes_links(context
)
1320 option
= self
.option
1321 selected
= [] # entry = index
1322 for si
, node
in enumerate(nodes
):
1324 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1328 fac
= nodes
[si
].inputs
[0]
1329 nodes
[si
].hide
= False
1330 if option
in {0.0, 1.0}:
1331 fac
.default_value
= option
1333 fac
.default_value
+= option
1338 class NWCopySettings(Operator
, NWBase
):
1339 bl_idname
= "node.nw_copy_settings"
1340 bl_label
= "Copy Settings"
1341 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1342 bl_options
= {'REGISTER', 'UNDO'}
1345 def poll(cls
, context
):
1346 return (nw_check(context
)
1347 and context
.active_node
is not None
1348 and context
.active_node
.type != 'FRAME')
1350 def execute(self
, context
):
1351 node_active
= context
.active_node
1352 node_selected
= context
.selected_nodes
1355 if not (len(node_selected
) > 1):
1356 self
.report({'ERROR'}, "2 nodes must be selected at least")
1357 return {'CANCELLED'}
1359 # Check if active node is in the selection
1360 selected_node_names
= [n
.name
for n
in node_selected
]
1361 if node_active
.name
not in selected_node_names
:
1362 self
.report({'ERROR'}, "No active node")
1363 return {'CANCELLED'}
1365 # Get nodes in selection by type
1366 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1368 if not (len(valid_nodes
) > 1) and node_active
:
1369 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1370 return {'CANCELLED'}
1372 if len(valid_nodes
) != len(node_selected
):
1373 # Report nodes that are not valid
1374 valid_node_names
= [n
.name
for n
in valid_nodes
]
1375 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1378 "Ignored {} (not of the same type as {})".format(
1379 ", ".join(not_valid_names
),
1382 # Reference original
1384 # node_selected_names = [n.name for n in node_selected]
1389 # Deselect all nodes
1390 for i
in node_selected
:
1393 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1394 # Run through all other nodes
1395 for node
in valid_nodes
[1:]:
1397 # Check for frame node
1398 parent
= node
.parent
if node
.parent
else None
1399 node_loc
= [node
.location
.x
, node
.location
.y
]
1401 # Select original to duplicate
1404 # Duplicate selected node
1405 bpy
.ops
.node
.duplicate()
1406 new_node
= context
.selected_nodes
[0]
1409 new_node
.select
= False
1411 # Properties to copy
1412 node_tree
= node
.id_data
1413 props_to_copy
= 'bl_idname name location height width'.split(' ')
1417 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1418 for i
in (i
for i
in mappings
if i
.is_linked
):
1420 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1423 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1424 props_to_copy
.pop(0)
1426 for prop
in props_to_copy
:
1427 setattr(new_node
, prop
, props
[prop
])
1429 # Get the node tree to remove the old node
1430 nodes
= node_tree
.nodes
1432 new_node
.name
= props
['name']
1435 new_node
.parent
= parent
1436 new_node
.location
= node_loc
1438 for str_from
, str_to
in reconnections
:
1439 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1441 success_names
.append(new_node
.name
)
1444 node_tree
.nodes
.active
= orig
1447 "Successfully copied attributes from {} to: {}".format(
1449 ", ".join(success_names
)))
1453 class NWCopyLabel(Operator
, NWBase
):
1454 bl_idname
= "node.nw_copy_label"
1455 bl_label
= "Copy Label"
1456 bl_options
= {'REGISTER', 'UNDO'}
1458 option
: EnumProperty(
1460 description
="Source of name of label",
1462 ('FROM_ACTIVE', 'from active', 'from active node',),
1463 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1464 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1468 def execute(self
, context
):
1469 nodes
, links
= get_nodes_links(context
)
1470 option
= self
.option
1471 active
= nodes
.active
1472 if option
== 'FROM_ACTIVE':
1474 src_label
= active
.label
1475 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1476 node
.label
= src_label
1477 elif option
== 'FROM_NODE':
1478 selected
= [n
for n
in nodes
if n
.select
]
1479 for node
in selected
:
1480 for input in node
.inputs
:
1482 src
= input.links
[0].from_node
1483 node
.label
= src
.label
1485 elif option
== 'FROM_SOCKET':
1486 selected
= [n
for n
in nodes
if n
.select
]
1487 for node
in selected
:
1488 for input in node
.inputs
:
1490 src
= input.links
[0].from_socket
1491 node
.label
= src
.name
1497 class NWClearLabel(Operator
, NWBase
):
1498 bl_idname
= "node.nw_clear_label"
1499 bl_label
= "Clear Label"
1500 bl_options
= {'REGISTER', 'UNDO'}
1502 option
: BoolProperty()
1504 def execute(self
, context
):
1505 nodes
, links
= get_nodes_links(context
)
1506 for node
in [n
for n
in nodes
if n
.select
]:
1511 def invoke(self
, context
, event
):
1513 return self
.execute(context
)
1515 return context
.window_manager
.invoke_confirm(self
, event
)
1518 class NWModifyLabels(Operator
, NWBase
):
1519 """Modify Labels of all selected nodes"""
1520 bl_idname
= "node.nw_modify_labels"
1521 bl_label
= "Modify Labels"
1522 bl_options
= {'REGISTER', 'UNDO'}
1524 prepend
: StringProperty(
1525 name
="Add to Beginning"
1527 append
: StringProperty(
1530 replace_from
: StringProperty(
1531 name
="Text to Replace"
1533 replace_to
: StringProperty(
1537 def execute(self
, context
):
1538 nodes
, links
= get_nodes_links(context
)
1539 for node
in [n
for n
in nodes
if n
.select
]:
1540 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1544 def invoke(self
, context
, event
):
1548 return context
.window_manager
.invoke_props_dialog(self
)
1551 class NWAddTextureSetup(Operator
, NWBase
):
1552 bl_idname
= "node.nw_add_texture"
1553 bl_label
= "Texture Setup"
1554 bl_description
= "Add Texture Node Setup to Selected Shaders"
1555 bl_options
= {'REGISTER', 'UNDO'}
1557 add_mapping
: BoolProperty(
1558 name
="Add Mapping Nodes",
1559 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1563 def poll(cls
, context
):
1564 return (nw_check(context
)
1565 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1567 def execute(self
, context
):
1568 nodes
, links
= get_nodes_links(context
)
1570 texture_types
= get_texture_node_types()
1571 selected_nodes
= [n
for n
in nodes
if n
.select
]
1573 for node
in selected_nodes
:
1578 target_input
= node
.inputs
[0]
1579 for input in node
.inputs
:
1582 if not input.is_linked
:
1583 target_input
= input
1586 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1591 locx
= node
.location
.x
1592 locy
= node
.location
.y
- (input_index
* padding
)
1594 is_texture_node
= node
.rna_type
.identifier
in texture_types
1595 use_environment_texture
= node
.type == 'BACKGROUND'
1597 # Add an image texture before normal shader nodes.
1598 if not is_texture_node
:
1599 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1600 image_texture_node
= nodes
.new(image_texture_type
)
1601 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1602 image_texture_node
.location
= [locx
- x_offset
, locy
]
1603 nodes
.active
= image_texture_node
1604 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1606 # The mapping setup following this will connect to the first input of this image texture.
1607 target_input
= image_texture_node
.inputs
[0]
1611 if is_texture_node
or self
.add_mapping
:
1613 mapping_node
= nodes
.new('ShaderNodeMapping')
1614 x_offset
= x_offset
+ mapping_node
.width
+ padding
1615 mapping_node
.location
= [locx
- x_offset
, locy
]
1616 connect_sockets(mapping_node
.outputs
[0], target_input
)
1618 # Add Texture Coordinates node.
1619 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1620 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1621 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1623 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1624 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1625 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1626 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1631 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1632 bl_idname
= "node.nw_add_textures_for_principled"
1633 bl_label
= "Principled Texture Setup"
1634 bl_description
= "Add Texture Node Setup for Principled BSDF"
1635 bl_options
= {'REGISTER', 'UNDO'}
1637 directory
: StringProperty(
1641 description
='Folder to search in for image files'
1643 files
: CollectionProperty(
1644 type=bpy
.types
.OperatorFileListElement
,
1645 options
={'HIDDEN', 'SKIP_SAVE'}
1648 relative_path
: BoolProperty(
1649 name
='Relative Path',
1650 description
='Set the file path relative to the blend file, when possible',
1659 def draw(self
, context
):
1660 layout
= self
.layout
1661 layout
.alignment
= 'LEFT'
1663 layout
.prop(self
, 'relative_path')
1666 def poll(cls
, context
):
1667 return (nw_check(context
)
1668 and nw_check_space_type(cls
, context
, 'ShaderNodeTree'))
1670 def execute(self
, context
):
1671 # Check if everything is ok
1672 if not self
.directory
:
1673 self
.report({'INFO'}, 'No Folder Selected')
1674 return {'CANCELLED'}
1675 if not self
.files
[:]:
1676 self
.report({'INFO'}, 'No Files Selected')
1677 return {'CANCELLED'}
1679 nodes
, links
= get_nodes_links(context
)
1680 active_node
= nodes
.active
1681 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1682 self
.report({'INFO'}, 'Select Principled BSDF')
1683 return {'CANCELLED'}
1685 # Filter textures names for texturetypes in filenames
1686 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1687 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1688 normal_abbr
= tags
.normal
.split(' ')
1689 bump_abbr
= tags
.bump
.split(' ')
1690 gloss_abbr
= tags
.gloss
.split(' ')
1691 rough_abbr
= tags
.rough
.split(' ')
1693 ['Displacement', tags
.displacement
.split(' '), None],
1694 ['Base Color', tags
.base_color
.split(' '), None],
1695 ['Metallic', tags
.metallic
.split(' '), None],
1696 ['Specular IOR Level', tags
.specular
.split(' '), None],
1697 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1698 ['Normal', normal_abbr
+ bump_abbr
, None],
1699 ['Transmission Weight', tags
.transmission
.split(' '), None],
1700 ['Emission Color', tags
.emission
.split(' '), None],
1701 ['Alpha', tags
.alpha
.split(' '), None],
1702 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1705 match_files_to_socket_names(self
.files
, socketnames
)
1706 # Remove socketnames without found files
1707 socketnames
= [s
for s
in socketnames
if s
[2]
1708 and path
.exists(self
.directory
+ s
[2])]
1710 self
.report({'INFO'}, 'No matching images found')
1711 print('No matching images found')
1712 return {'CANCELLED'}
1714 # Don't override path earlier as os.path is used to check the absolute path
1715 import_path
= self
.directory
1716 if self
.relative_path
:
1717 if bpy
.data
.filepath
:
1719 import_path
= bpy
.path
.relpath(self
.directory
)
1724 print('\nMatched Textures:')
1729 roughness_node
= None
1730 for i
, sname
in enumerate(socketnames
):
1731 print(i
, sname
[0], sname
[2])
1733 # DISPLACEMENT NODES
1734 if sname
[0] == 'Displacement':
1735 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1736 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1737 disp_texture
.image
= img
1738 disp_texture
.label
= 'Displacement'
1739 if disp_texture
.image
:
1740 disp_texture
.image
.colorspace_settings
.is_data
= True
1742 # Add displacement offset nodes
1743 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1744 # Align the Displacement node under the active Principled BSDF node
1745 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1746 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1748 # TODO Turn on true displacement in the material
1749 # Too complicated for now
1752 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1754 if not output_node
[0].inputs
[2].is_linked
:
1755 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1759 # AMBIENT OCCLUSION TEXTURE
1760 if sname
[0] == 'Ambient Occlusion':
1761 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1762 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1763 ao_texture
.image
= img
1764 ao_texture
.label
= sname
[0]
1765 if ao_texture
.image
:
1766 ao_texture
.image
.colorspace_settings
.is_data
= True
1770 if not active_node
.inputs
[sname
[0]].is_linked
:
1771 # No texture node connected -> add texture node with new image
1772 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1773 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1774 texture_node
.image
= img
1777 if sname
[0] == 'Normal':
1778 # Test if new texture node is normal or bump map
1779 fname_components
= split_into_components(sname
[2])
1780 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1781 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1783 # If Normal add normal node in between
1784 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1785 link
= connect_sockets(normal_node
.inputs
[1], texture_node
.outputs
[0])
1787 # If Bump add bump node in between
1788 normal_node
= nodes
.new(type='ShaderNodeBump')
1789 link
= connect_sockets(normal_node
.inputs
[2], texture_node
.outputs
[0])
1791 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1792 normal_node_texture
= texture_node
1794 elif sname
[0] == 'Roughness':
1795 # Test if glossy or roughness map
1796 fname_components
= split_into_components(sname
[2])
1797 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1798 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1801 # If Roughness nothing to to
1802 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1805 # If Gloss Map add invert node
1806 invert_node
= nodes
.new(type='ShaderNodeInvert')
1807 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1809 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1810 roughness_node
= texture_node
1813 # This is a simple connection Texture --> Input slot
1814 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1816 # Use non-color except for color inputs
1817 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1818 texture_node
.image
.colorspace_settings
.is_data
= True
1821 # If already texture connected. add to node list for alignment
1822 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1824 # This are all connected texture nodes
1825 texture_nodes
.append(texture_node
)
1826 texture_node
.label
= sname
[0]
1829 texture_nodes
.append(disp_texture
)
1832 # We want the ambient occlusion texture to be the top most texture node
1833 texture_nodes
.insert(0, ao_texture
)
1836 for i
, texture_node
in enumerate(texture_nodes
):
1837 offset
= Vector((-550, (i
* -280) + 200))
1838 texture_node
.location
= active_node
.location
+ offset
1841 # Extra alignment if normal node was added
1842 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1845 # Alignment of invert node if glossy map
1846 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1848 # Add texture input + mapping
1849 mapping
= nodes
.new(type='ShaderNodeMapping')
1850 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1851 if len(texture_nodes
) > 1:
1852 # If more than one texture add reroute node in between
1853 reroute
= nodes
.new(type='NodeReroute')
1854 texture_nodes
.append(reroute
)
1855 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1856 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1857 reroute
.location
= tex_coords
+ Vector((-50, -120))
1858 for texture_node
in texture_nodes
:
1859 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1860 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1862 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1864 # Connect texture_coordiantes to mapping node
1865 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1866 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1867 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1869 # Create frame around tex coords and mapping
1870 frame
= nodes
.new(type='NodeFrame')
1871 frame
.label
= 'Mapping'
1872 mapping
.parent
= frame
1873 texture_input
.parent
= frame
1876 # Create frame around texture nodes
1877 frame
= nodes
.new(type='NodeFrame')
1878 frame
.label
= 'Textures'
1879 for tnode
in texture_nodes
:
1880 tnode
.parent
= frame
1884 active_node
.select
= False
1887 force_update(context
)
1891 class NWAddReroutes(Operator
, NWBase
):
1892 """Add Reroute Nodes and link them to outputs of selected nodes"""
1893 bl_idname
= "node.nw_add_reroutes"
1894 bl_label
= "Add Reroutes"
1895 bl_description
= "Add Reroutes to Outputs"
1896 bl_options
= {'REGISTER', 'UNDO'}
1898 option
: EnumProperty(
1901 ('ALL', 'to all', 'Add to all outputs'),
1902 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1903 ('LINKED', 'to linked', 'Add only to linked outputs'),
1907 def execute(self
, context
):
1908 tree_type
= context
.space_data
.node_tree
.type
1909 option
= self
.option
1910 nodes
, links
= get_nodes_links(context
)
1911 # output valid when option is 'all' or when 'loose' output has no links
1913 post_select
= [] # nodes to be selected after execution
1914 # create reroutes and recreate links
1915 for node
in [n
for n
in nodes
if n
.select
]:
1920 # unhide 'REROUTE' nodes to avoid issues with location.y
1921 if node
.type == 'REROUTE':
1923 # Hack needed to calculate real width
1925 bpy
.ops
.node
.select_all(action
='DESELECT')
1926 helper
= nodes
.new('NodeReroute')
1927 helper
.select
= True
1929 # resize node and helper to zero. Then check locations to calculate width
1930 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
1931 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
1932 # restore node location
1933 node
.location
= x
, y
1936 # only helper is selected now
1937 bpy
.ops
.node
.delete()
1938 x
= node
.location
.x
+ width
+ 20.0
1939 if node
.type != 'REROUTE':
1943 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
1944 for out_i
, output
in enumerate(node
.outputs
):
1945 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
1946 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
1947 if node
.type != 'R_LAYERS':
1949 else: # if 'R_LAYERS' check if output represent used render pass
1950 node_scene
= node
.scene
1951 node_layer
= node
.layer
1952 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
1953 if output
.name
== 'Alpha':
1956 # check entries in global 'rl_outputs' variable
1957 for rlo
in rl_outputs
:
1958 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
1959 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
1962 valid
= ((option
== 'ALL') or
1963 (option
== 'LOOSE' and not output
.links
) or
1964 (option
== 'LINKED' and output
.links
))
1965 # Add reroutes only if valid, but offset location in all cases.
1967 n
= nodes
.new('NodeReroute')
1969 for link
in output
.links
:
1970 connect_sockets(n
.outputs
[0], link
.to_socket
)
1971 connect_sockets(output
, n
.inputs
[0])
1973 post_select
.append(n
)
1977 # disselect the node so that after execution of script only newly created nodes are selected
1979 # nicer reroutes distribution along y when node.hide
1981 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
1982 for reroute
in [r
for r
in nodes
if r
.select
]:
1983 reroute
.location
.y
-= y_translate
1984 for node
in post_select
:
1990 class NWLinkActiveToSelected(Operator
, NWBase
):
1991 """Link active node to selected nodes basing on various criteria"""
1992 bl_idname
= "node.nw_link_active_to_selected"
1993 bl_label
= "Link Active Node to Selected"
1994 bl_options
= {'REGISTER', 'UNDO'}
1996 replace
: BoolProperty()
1997 use_node_name
: BoolProperty()
1998 use_outputs_names
: BoolProperty()
2001 def poll(cls
, context
):
2002 return (nw_check(context
)
2003 and context
.active_node
is not None
2004 and context
.active_node
.select
)
2006 def execute(self
, context
):
2007 nodes
, links
= get_nodes_links(context
)
2008 replace
= self
.replace
2009 use_node_name
= self
.use_node_name
2010 use_outputs_names
= self
.use_outputs_names
2011 active
= nodes
.active
2012 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2013 outputs
= [] # Only usable outputs of active nodes will be stored here.
2014 for out
in active
.outputs
:
2015 if active
.type != 'R_LAYERS':
2018 # 'R_LAYERS' node type needs special handling.
2019 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2020 # Only outputs that represent used passes should be taken into account
2021 # Check if pass represented by output is used.
2022 # global 'rl_outputs' list will be used for that
2023 for rlo
in rl_outputs
:
2024 pass_used
= False # initial value. Will be set to True if pass is used
2025 if out
.name
== 'Alpha':
2026 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2028 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2029 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2030 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2034 doit
= True # Will be changed to False when links successfully added to previous output.
2037 for node
in selected
:
2038 dst_name
= node
.name
# Will be compared with src_name if needed.
2039 # When node has label - use it as dst_name
2041 dst_name
= node
.label
2042 valid
= True # Initial value. Will be changed to False if names don't match.
2043 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2045 # Set src_name to source node name or label
2046 src_name
= active
.name
2048 src_name
= active
.label
2049 elif use_outputs_names
:
2050 src_name
= (out
.name
, )
2051 for rlo
in rl_outputs
:
2052 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2053 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2054 if dst_name
not in src_name
:
2057 for input in node
.inputs
:
2058 if input.type == out
.type or node
.type == 'REROUTE':
2059 if replace
or not input.is_linked
:
2060 connect_sockets(out
, input)
2061 if not use_node_name
and not use_outputs_names
:
2068 class NWAlignNodes(Operator
, NWBase
):
2069 '''Align the selected nodes neatly in a row/column'''
2070 bl_idname
= "node.nw_align_nodes"
2071 bl_label
= "Align Nodes"
2072 bl_options
= {'REGISTER', 'UNDO'}
2073 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2075 def execute(self
, context
):
2076 nodes
, links
= get_nodes_links(context
)
2077 margin
= self
.margin
2081 if node
.select
and node
.type != 'FRAME':
2082 selection
.append(node
)
2084 # If no nodes are selected, align all nodes
2088 elif nodes
.active
in selection
:
2089 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2091 # Check if nodes should be laid out horizontally or vertically
2092 # use dimension to get center of node, not corner
2093 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2094 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2095 x_range
= max(x_locs
) - min(x_locs
)
2096 y_range
= max(y_locs
) - min(y_locs
)
2097 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2098 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2099 horizontal
= x_range
> y_range
2101 # Sort selection by location of node mid-point
2103 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2105 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2109 for node
in selection
:
2110 current_margin
= margin
2111 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2114 node
.location
.x
= current_pos
2115 current_pos
+= current_margin
+ node
.dimensions
.x
2116 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2118 node
.location
.y
= current_pos
2119 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2120 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2122 # If active node is selected, center nodes around it
2123 if active_loc
is not None:
2124 active_loc_diff
= active_loc
- nodes
.active
.location
2125 for node
in selection
:
2126 node
.location
+= active_loc_diff
2127 else: # Position nodes centered around where they used to be
2128 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2129 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2130 new_mid
= (max(locs
) + min(locs
)) / 2
2131 for node
in selection
:
2133 node
.location
.x
+= (mid_x
- new_mid
)
2135 node
.location
.y
+= (mid_y
- new_mid
)
2140 class NWSelectParentChildren(Operator
, NWBase
):
2141 bl_idname
= "node.nw_select_parent_child"
2142 bl_label
= "Select Parent or Children"
2143 bl_options
= {'REGISTER', 'UNDO'}
2145 option
: EnumProperty(
2148 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2149 ('CHILD', 'Select Children', 'Select members of selected frame'),
2153 def execute(self
, context
):
2154 nodes
, links
= get_nodes_links(context
)
2155 option
= self
.option
2156 selected
= [node
for node
in nodes
if node
.select
]
2157 if option
== 'PARENT':
2158 for sel
in selected
:
2161 parent
.select
= True
2162 else: # option == 'CHILD'
2163 for sel
in selected
:
2164 children
= [node
for node
in nodes
if node
.parent
== sel
]
2165 for kid
in children
:
2171 class NWDetachOutputs(Operator
, NWBase
):
2172 """Detach outputs of selected node leaving inputs linked"""
2173 bl_idname
= "node.nw_detach_outputs"
2174 bl_label
= "Detach Outputs"
2175 bl_options
= {'REGISTER', 'UNDO'}
2177 def execute(self
, context
):
2178 nodes
, links
= get_nodes_links(context
)
2179 selected
= context
.selected_nodes
2180 bpy
.ops
.node
.duplicate_move_keep_inputs()
2181 new_nodes
= context
.selected_nodes
2182 bpy
.ops
.node
.select_all(action
="DESELECT")
2183 for node
in selected
:
2185 bpy
.ops
.node
.delete_reconnect()
2186 for new_node
in new_nodes
:
2187 new_node
.select
= True
2188 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2193 class NWLinkToOutputNode(Operator
):
2194 """Link to Composite node or Material Output node"""
2195 bl_idname
= "node.nw_link_out"
2196 bl_label
= "Connect to Output"
2197 bl_options
= {'REGISTER', 'UNDO'}
2200 def poll(cls
, context
):
2201 """Disabled for custom nodes as we do not know which nodes are outputs."""
2202 return (nw_check(context
)
2203 and nw_check_space_type(cls
, context
, 'ShaderNodeTree', 'CompositorNodeTree',
2204 'TextureNodeTree', 'GeometryNodeTree')
2205 and context
.active_node
is not None
2206 and any(is_visible_socket(out
) for out
in context
.active_node
.outputs
))
2208 def execute(self
, context
):
2209 nodes
, links
= get_nodes_links(context
)
2210 active
= nodes
.active
2212 tree_type
= context
.space_data
.tree_type
2213 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2214 'WORLD': 'ShaderNodeOutputWorld',
2215 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2217 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2218 'CompositorNodeTree': 'CompositorNodeComposite',
2219 'TextureNodeTree': 'TextureNodeOutput',
2220 'GeometryNodeTree': 'NodeGroupOutput',
2223 # check whether the node is an output node and,
2224 # if supported, whether it's the active one
2225 if node
.rna_type
.identifier
== output_type \
2226 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2230 else: # No output node exists
2231 bpy
.ops
.node
.select_all(action
="DESELECT")
2232 output_node
= nodes
.new(output_type
)
2233 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2234 output_node
.location
.y
= active
.location
.y
2237 for i
, output
in enumerate(active
.outputs
):
2238 if is_visible_socket(output
):
2241 for i
, output
in enumerate(active
.outputs
):
2242 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2247 if tree_type
== 'ShaderNodeTree':
2248 if active
.outputs
[output_index
].name
== 'Volume':
2250 elif active
.outputs
[output_index
].name
== 'Displacement':
2252 elif tree_type
== 'GeometryNodeTree':
2253 if active
.outputs
[output_index
].type != 'GEOMETRY':
2254 return {'CANCELLED'}
2255 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2257 force_update(context
) # viewport render does not update
2262 class NWMakeLink(Operator
, NWBase
):
2263 """Make a link from one socket to another"""
2264 bl_idname
= 'node.nw_make_link'
2265 bl_label
= 'Make Link'
2266 bl_options
= {'REGISTER', 'UNDO'}
2267 from_socket
: IntProperty()
2268 to_socket
: IntProperty()
2270 def execute(self
, context
):
2271 nodes
, links
= get_nodes_links(context
)
2273 n1
= nodes
[context
.scene
.NWLazySource
]
2274 n2
= nodes
[context
.scene
.NWLazyTarget
]
2276 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2278 force_update(context
)
2283 class NWCallInputsMenu(Operator
, NWBase
):
2284 """Link from this output"""
2285 bl_idname
= 'node.nw_call_inputs_menu'
2286 bl_label
= 'Make Link'
2287 bl_options
= {'REGISTER', 'UNDO'}
2288 from_socket
: IntProperty()
2290 def execute(self
, context
):
2291 nodes
, links
= get_nodes_links(context
)
2293 context
.scene
.NWSourceSocket
= self
.from_socket
2295 n1
= nodes
[context
.scene
.NWLazySource
]
2296 n2
= nodes
[context
.scene
.NWLazyTarget
]
2297 if len(n2
.inputs
) > 1:
2298 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2299 elif len(n2
.inputs
) == 1:
2300 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2304 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2305 """Add an Image Sequence"""
2306 bl_idname
= 'node.nw_add_sequence'
2307 bl_label
= 'Import Image Sequence'
2308 bl_options
= {'REGISTER', 'UNDO'}
2310 directory
: StringProperty(
2313 filename
: StringProperty(
2316 files
: CollectionProperty(
2317 type=bpy
.types
.OperatorFileListElement
,
2318 options
={'HIDDEN', 'SKIP_SAVE'}
2320 relative_path
: BoolProperty(
2321 name
='Relative Path',
2322 description
='Set the file path relative to the blend file, when possible',
2326 def draw(self
, context
):
2327 layout
= self
.layout
2328 layout
.alignment
= 'LEFT'
2330 layout
.prop(self
, 'relative_path')
2332 def execute(self
, context
):
2333 nodes
, links
= get_nodes_links(context
)
2334 directory
= self
.directory
2335 filename
= self
.filename
2337 tree
= context
.space_data
.node_tree
2340 # print ("\nDIR:", directory)
2341 # print ("FN:", filename)
2342 # print ("Fs:", list(f.name for f in files), '\n')
2344 if tree
.type == 'SHADER':
2345 node_type
= "ShaderNodeTexImage"
2346 elif tree
.type == 'COMPOSITING':
2347 node_type
= "CompositorNodeImage"
2349 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2350 return {'CANCELLED'}
2352 if not files
[0].name
and not filename
:
2353 self
.report({'ERROR'}, "No file chosen")
2354 return {'CANCELLED'}
2355 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2356 # User has selected multiple files without an active one, or the active one is non-existent
2357 filename
= files
[0].name
2359 if not path
.exists(directory
+ filename
):
2360 self
.report({'ERROR'}, filename
+ " does not exist!")
2361 return {'CANCELLED'}
2363 without_ext
= '.'.join(filename
.split('.')[:-1])
2365 # if last digit isn't a number, it's not a sequence
2366 if not without_ext
[-1].isdigit():
2367 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2368 return {'CANCELLED'}
2370 extension
= filename
.split('.')[-1]
2371 reverse
= without_ext
[::-1] # reverse string
2374 for char
in reverse
:
2380 without_num
= without_ext
[:count_numbers
* -1]
2382 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2384 num_frames
= len(files
)
2386 nodes_list
= [node
for node
in nodes
]
2388 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2389 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2393 yloc
+= node_mid_pt(node
, 'y')
2394 yloc
= yloc
/ len(nodes
)
2399 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2401 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2403 node
.label
= name_with_hashes
2405 filepath
= directory
+ (without_ext
+ '.' + extension
)
2406 if self
.relative_path
:
2407 if bpy
.data
.filepath
:
2409 filepath
= bpy
.path
.relpath(filepath
)
2413 img
= bpy
.data
.images
.load(filepath
)
2414 img
.source
= 'SEQUENCE'
2415 img
.name
= name_with_hashes
2417 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2418 # separate the number from the file name of the first file
2419 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2420 image_user
.frame_duration
= num_frames
2425 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2426 """Add multiple images at once"""
2427 bl_idname
= 'node.nw_add_multiple_images'
2428 bl_label
= 'Open Selected Images'
2429 bl_options
= {'REGISTER', 'UNDO'}
2430 directory
: StringProperty(
2433 files
: CollectionProperty(
2434 type=bpy
.types
.OperatorFileListElement
,
2435 options
={'HIDDEN', 'SKIP_SAVE'}
2438 def execute(self
, context
):
2439 nodes
, links
= get_nodes_links(context
)
2441 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2443 if context
.space_data
.node_tree
.type == 'SHADER':
2444 node_type
= "ShaderNodeTexImage"
2445 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2446 node_type
= "CompositorNodeImage"
2448 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2449 return {'CANCELLED'}
2452 for f
in self
.files
:
2455 node
= nodes
.new(node_type
)
2456 new_nodes
.append(node
)
2459 node
.location
.x
= xloc
2460 node
.location
.y
= yloc
2463 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2466 # shift new nodes up to center of tree
2467 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2469 if node
in new_nodes
:
2471 node
.location
.y
+= (list_size
/ 2)
2477 class NWViewerFocus(bpy
.types
.Operator
):
2478 """Set the viewer tile center to the mouse position"""
2479 bl_idname
= "node.nw_viewer_focus"
2480 bl_label
= "Viewer Focus"
2482 x
: bpy
.props
.IntProperty()
2483 y
: bpy
.props
.IntProperty()
2486 def poll(cls
, context
):
2487 return (nw_check(context
)
2488 and nw_check_space_type(cls
, context
, 'CompositorNodeTree'))
2490 def execute(self
, context
):
2493 def invoke(self
, context
, event
):
2494 render
= context
.scene
.render
2495 space
= context
.space_data
2496 percent
= render
.resolution_percentage
* 0.01
2498 nodes
, links
= get_nodes_links(context
)
2499 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2502 mlocx
= event
.mouse_region_x
2503 mlocy
= event
.mouse_region_y
2504 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2506 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2507 region_x
= context
.region
.width
2508 region_y
= context
.region
.height
2510 region_center_x
= context
.region
.width
/ 2
2511 region_center_y
= context
.region
.height
/ 2
2513 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2514 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2516 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2517 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2519 margin_x
= region_center_x
- backdrop_center_x
2520 margin_y
= region_center_y
- backdrop_center_y
2522 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2523 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2525 for node
in viewers
:
2526 node
.center_x
= abs_mouse_x
2527 node
.center_y
= abs_mouse_y
2529 return {'PASS_THROUGH'}
2531 return self
.execute(context
)
2534 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2535 """Save the current viewer node to an image file"""
2536 bl_idname
= "node.nw_save_viewer"
2537 bl_label
= "Save This Image"
2538 filepath
: StringProperty(subtype
="FILE_PATH")
2539 filename_ext
: EnumProperty(
2541 description
="Choose the file format to save to",
2542 items
=(('.bmp', "BMP", ""),
2543 ('.rgb', 'IRIS', ""),
2544 ('.png', 'PNG', ""),
2545 ('.jpg', 'JPEG', ""),
2546 ('.jp2', 'JPEG2000', ""),
2547 ('.tga', 'TARGA', ""),
2548 ('.cin', 'CINEON', ""),
2549 ('.dpx', 'DPX', ""),
2550 ('.exr', 'OPEN_EXR', ""),
2551 ('.hdr', 'HDR', ""),
2552 ('.tif', 'TIFF', "")),
2557 def poll(cls
, context
):
2558 return (nw_check(context
)
2559 and nw_check_space_type(cls
, context
, 'CompositorNodeTree')
2560 and any(img
.source
== 'VIEWER'
2561 and img
.render_slots
== 0
2562 for img
in bpy
.data
.images
)
2563 and sum(bpy
.data
.images
["Viewer Node"].size
) > 0) # False if not connected or connected but no image
2565 def execute(self
, context
):
2582 basename
, ext
= path
.splitext(fp
)
2583 old_render_format
= context
.scene
.render
.image_settings
.file_format
2584 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2585 context
.area
.type = "IMAGE_EDITOR"
2586 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2587 context
.area
.spaces
[0].image
.save_render(fp
)
2588 context
.area
.type = "NODE_EDITOR"
2589 context
.scene
.render
.image_settings
.file_format
= old_render_format
2593 class NWResetNodes(bpy
.types
.Operator
):
2594 """Reset Nodes in Selection"""
2595 bl_idname
= "node.nw_reset_nodes"
2596 bl_label
= "Reset Nodes"
2597 bl_options
= {'REGISTER', 'UNDO'}
2600 def poll(cls
, context
):
2601 space
= context
.space_data
2602 return space
.type == 'NODE_EDITOR'
2604 def execute(self
, context
):
2605 node_active
= context
.active_node
2606 node_selected
= context
.selected_nodes
2607 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2609 # Check if one node is selected at least
2610 if not (len(node_selected
) > 0):
2611 self
.report({'ERROR'}, "1 node must be selected at least")
2612 return {'CANCELLED'}
2614 active_node_name
= node_active
.name
if node_active
.select
else None
2615 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2617 # Create output lists
2618 selected_node_names
= [n
.name
for n
in node_selected
]
2621 # Reset all valid children in a frame
2622 node_active_is_frame
= False
2623 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2624 node_tree
= node_active
.id_data
2625 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2627 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2628 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2629 node_active_is_frame
= True
2631 # Check if valid nodes in selection
2632 if not (len(valid_nodes
) > 0):
2633 # Check for frames only
2634 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2635 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2636 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2638 self
.report({'ERROR'}, "No valid node(s) in selection")
2639 return {'CANCELLED'}
2641 # Report nodes that are not valid
2642 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2643 valid_node_names
= [n
.name
for n
in valid_nodes
]
2644 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2645 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2647 # Deselect all nodes
2648 for i
in node_selected
:
2651 # Run through all valid nodes
2652 for node
in valid_nodes
:
2654 parent
= node
.parent
if node
.parent
else None
2655 node_loc
= [node
.location
.x
, node
.location
.y
]
2657 node_tree
= node
.id_data
2658 props_to_copy
= 'bl_idname name location height width'.split(' ')
2661 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2662 for i
in (i
for i
in mappings
if i
.is_linked
):
2664 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2666 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2668 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2669 props_to_copy
.pop(0)
2671 for prop
in props_to_copy
:
2672 setattr(new_node
, prop
, props
[prop
])
2674 nodes
= node_tree
.nodes
2676 new_node
.name
= props
['name']
2679 new_node
.parent
= parent
2680 new_node
.location
= node_loc
2682 for str_from
, str_to
in reconnections
:
2683 connect_sockets(eval(str_from
), eval(str_to
))
2685 new_node
.select
= False
2686 success_names
.append(new_node
.name
)
2688 # Reselect all nodes
2689 if selected_node_names
and node_active_is_frame
is False:
2690 for i
in selected_node_names
:
2691 node_tree
.nodes
[i
].select
= True
2693 if active_node_name
is not None:
2694 node_tree
.nodes
[active_node_name
].select
= True
2695 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2697 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2719 NWAddPrincipledSetup
,
2721 NWLinkActiveToSelected
,
2723 NWSelectParentChildren
,
2729 NWAddMultipleImages
,
2737 from bpy
.utils
import register_class
2743 from bpy
.utils
import unregister_class
2746 unregister_class(cls
)