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
)
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
.is_socket_used_other_mats(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(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 is_socket_used_other_mats(self
, socket
):
629 """Ensure used sockets in other materials are calculated and check given socket"""
630 if not hasattr(self
, "used_viewer_sockets_other_mats"):
631 self
.used_viewer_sockets_other_mats
= []
632 for mat
in bpy
.data
.materials
:
633 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
636 output_node
= get_group_output_node(mat
.node_tree
,
637 output_node_type
=self
.shader_output_type
)
638 if output_node
is not None:
639 self
.search_sockets(output_node
, self
.used_viewer_sockets_other_mats
)
640 return socket
in self
.used_viewer_sockets_other_mats
642 def get_output_index(self
, node
, output_node
, is_base_node_tree
, socket_type
, check_type
=False):
643 """Get the next available output socket in the active node"""
646 for i
, out
in enumerate(node
.outputs
):
647 if is_visible_socket(out
) and (not check_type
or out
.type == socket_type
):
648 valid_outputs
.append(i
)
650 out_i
= valid_outputs
[0] # Start index of node's outputs
651 for i
, valid_i
in enumerate(valid_outputs
):
652 for out_link
in node
.outputs
[valid_i
].links
:
653 if is_viewer_link(out_link
, output_node
):
654 if is_base_node_tree
or self
.link_leads_to_used_socket(out_link
):
655 if i
< len(valid_outputs
) - 1:
656 out_i
= valid_outputs
[i
+ 1]
658 out_i
= valid_outputs
[0]
661 def create_links(self
, path
, node
, active_node_socket_id
, socket_type
):
662 """Create links at each step in the node group path."""
663 path
= list(reversed(path
))
664 # Starting from the level of the active node
665 for path_index
, path_element
in enumerate(path
[:-1]):
666 # Ensure there is a viewer node and it has an input
667 tree
= path_element
.node_tree
668 viewer_socket
= self
.ensure_viewer_socket(
670 connect_socket
= node
.outputs
[active_node_socket_id
]
671 if path_index
== 0 else None)
672 if viewer_socket
in self
.delete_sockets
:
673 self
.delete_sockets
.remove(viewer_socket
)
675 # Connect the current to its viewer
676 link_start
= node
.outputs
[active_node_socket_id
]
677 link_end
= self
.ensure_group_output(tree
).inputs
[viewer_socket
.identifier
]
678 connect_sockets(link_start
, link_end
)
680 # Go up in the node group hierarchy
681 next_tree
= path
[path_index
+ 1].node_tree
682 node
= next(n
for n
in next_tree
.nodes
684 and n
.node_tree
== tree
)
686 active_node_socket_id
= viewer_socket
.identifier
687 return node
.outputs
[active_node_socket_id
]
691 for socket
in self
.delete_sockets
:
692 if not self
.is_socket_used_other_mats(socket
):
693 tree
= socket
.id_data
694 self
.remove_socket(tree
, socket
)
696 def invoke(self
, context
, event
):
697 space
= context
.space_data
698 # Ignore operator when running in wrong context.
699 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
700 return {'PASS_THROUGH'}
702 mlocx
= event
.mouse_region_x
703 mlocy
= event
.mouse_region_y
704 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
705 if 'FINISHED' not in select_node
: # only run if mouse click is on a node
708 base_node_tree
= space
.node_tree
709 active_tree
= context
.space_data
.edit_tree
710 path
= context
.space_data
.path
711 nodes
= active_tree
.nodes
712 active
= nodes
.active
714 if not active
and not any(is_visible_socket(out
) for out
in active
.outputs
):
717 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
718 self
.delete_sockets
= []
719 self
.scan_nodes(base_node_tree
, self
.delete_sockets
)
721 if not active
.outputs
:
725 # For geometry node trees, we just connect to the group output
726 if space
.tree_type
== "GeometryNodeTree":
727 socket_type
= 'NodeSocketGeometry'
729 # Find (or create if needed) the output of this node tree
730 output_node
= self
.ensure_group_output(base_node_tree
)
732 active_node_socket_index
= self
.get_output_index(
733 active
, output_node
, base_node_tree
== active_tree
, 'GEOMETRY', check_type
=True
735 # If there is no 'GEOMETRY' output type - We can't preview the node
736 if active_node_socket_index
is None:
739 # Find an input socket of the output of type geometry
740 output_node_socket_index
= None
741 for i
, inp
in enumerate(output_node
.inputs
):
742 if inp
.type == 'GEOMETRY':
743 output_node_socket_index
= i
745 if output_node_socket_index
is None:
746 # Create geometry socket
747 geometry_out_socket
= base_node_tree
.interface
.new_socket(
748 'Geometry', in_out
='OUTPUT', socket_type
=socket_type
750 output_node_socket_index
= geometry_out_socket
.index
752 # For shader node trees, we connect to a material output
753 elif space
.tree_type
== "ShaderNodeTree":
754 socket_type
= 'NodeSocketShader'
755 self
.init_shader_variables(space
, space
.shader_type
)
757 # Get or create material_output node
758 output_node
= get_group_output_node(base_node_tree
,
759 output_node_type
=self
.shader_output_type
)
761 output_node
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
762 output_node
.location
= get_output_location(base_node_tree
)
763 output_node
.select
= False
765 active_node_socket_index
= self
.get_output_index(
766 active
, output_node
, base_node_tree
== active_tree
, 'SHADER'
769 # Cancel if no socket was found. This can happen for group input
770 # nodes with only a virtual socket output.
771 if active_node_socket_index
is None:
774 if active
.outputs
[active_node_socket_index
].name
== "Volume":
775 output_node_socket_index
= 1
777 output_node_socket_index
= 0
779 # If there are no nested node groups, the link starts at the active node
780 node_output
= active
.outputs
[active_node_socket_index
]
782 # Recursively connect inside nested node groups and get the one from base level
783 node_output
= self
.create_links(path
, active
, active_node_socket_index
, socket_type
)
784 output_node_input
= output_node
.inputs
[output_node_socket_index
]
786 # Connect at base level
787 connect_sockets(node_output
, output_node_input
)
790 nodes
.active
= active
792 force_update(context
)
796 class NWFrameSelected(Operator
, NWBase
):
797 bl_idname
= "node.nw_frame_selected"
798 bl_label
= "Frame Selected"
799 bl_description
= "Add a frame node and parent the selected nodes to it"
800 bl_options
= {'REGISTER', 'UNDO'}
802 label_prop
: StringProperty(
804 description
='The visual name of the frame node',
807 use_custom_color_prop
: BoolProperty(
809 description
="Use custom color for the frame node",
812 color_prop
: FloatVectorProperty(
814 description
="The color of the frame node",
815 default
=(0.604, 0.604, 0.604),
816 min=0, max=1, step
=1, precision
=3,
817 subtype
='COLOR_GAMMA', size
=3
820 def draw(self
, context
):
822 layout
.prop(self
, 'label_prop')
823 layout
.prop(self
, 'use_custom_color_prop')
824 col
= layout
.column()
825 col
.active
= self
.use_custom_color_prop
826 col
.prop(self
, 'color_prop', text
="")
828 def execute(self
, context
):
829 nodes
, links
= get_nodes_links(context
)
833 selected
.append(node
)
835 bpy
.ops
.node
.add_node(type='NodeFrame')
837 frm
.label
= self
.label_prop
838 frm
.use_custom_color
= self
.use_custom_color_prop
839 frm
.color
= self
.color_prop
841 for node
in selected
:
847 class NWReloadImages(Operator
):
848 bl_idname
= "node.nw_reload_images"
849 bl_label
= "Reload Images"
850 bl_description
= "Update all the image nodes to match their files on disk"
853 def poll(cls
, context
):
854 """Disabled for custom nodes."""
855 return (nw_check(cls
, context
)
856 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
857 'TextureNodeTree', 'GeometryNodeTree'}))
859 def execute(self
, context
):
860 nodes
, links
= get_nodes_links(context
)
861 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
864 if node
.type in image_types
:
865 if node
.type == "TEXTURE":
866 if node
.texture
: # node has texture assigned
867 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
868 if node
.texture
.image
: # texture has image assigned
869 node
.texture
.image
.reload()
877 self
.report({'INFO'}, "Reloaded images")
878 print("Reloaded " + str(num_reloaded
) + " images")
879 force_update(context
)
882 self
.report({'WARNING'}, "No images found to reload in this node tree")
886 class NWMergeNodes(Operator
, NWBase
):
887 bl_idname
= "node.nw_merge_nodes"
888 bl_label
= "Merge Nodes"
889 bl_description
= "Merge Selected Nodes"
890 bl_options
= {'REGISTER', 'UNDO'}
894 description
="All possible blend types, boolean operations and math operations",
895 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
],
897 merge_type
: EnumProperty(
899 description
="Type of Merge to be used",
901 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
902 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
903 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
904 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
905 ('MATH', 'Math Node', 'Merge using Math Nodes'),
906 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
907 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
911 # Check if the link connects to a node that is in selected_nodes
912 # If not, then check recursively for each link in the nodes outputs.
913 # If yes, return True. If the recursion stops without finding a node
914 # in selected_nodes, it returns False. The depth is used to prevent
915 # getting stuck in a loop because of an already present cycle.
917 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
919 # We're stuck in a cycle, but that cycle was already present,
920 # so we return False.
921 # NOTE: The number 255 is arbitrary, but seems to work well.
924 if node
in selected_nodes
:
928 for output
in node
.outputs
:
930 for olink
in output
.links
:
931 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
933 # None of the outputs found a node in selected_nodes, so there is no cycle.
936 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
937 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
938 # be connected. The last one is assumed to be a multi input socket.
939 # For convenience the node is returned.
941 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
942 # The y-location of the last node
943 loc_y
= nodes_list
[-1][2]
944 if merge_position
== 'CENTER':
945 # Average the y-location
946 for i
in range(len(nodes_list
) - 1):
947 loc_y
+= nodes_list
[i
][2]
948 loc_y
= loc_y
/ len(nodes_list
)
949 new_node
= nodes
.new(node_name
)
950 new_node
.hide
= do_hide
951 new_node
.location
.x
= loc_x
952 new_node
.location
.y
= loc_y
953 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
955 outputs_for_multi_input
= []
956 for i
, node
in enumerate(selected_nodes
):
958 # Search for the first node which had output links that do not create
959 # a cycle, which we can then reconnect afterwards.
960 if prev_links
== [] and node
.outputs
[0].is_linked
:
962 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
963 link
, selected_nodes
)]
964 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
965 # To get the placement to look right we need to reverse the order in which we connect the
966 # outputs to the multi input socket.
967 if i
< len(socket_indices
) - 1:
968 ind
= socket_indices
[i
]
969 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
971 outputs_for_multi_input
.insert(0, node
.outputs
[0])
972 if outputs_for_multi_input
!= []:
973 ind
= socket_indices
[-1]
974 for output
in outputs_for_multi_input
:
975 connect_sockets(output
, new_node
.inputs
[ind
])
977 for link
in prev_links
:
978 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
982 def poll(cls
, context
):
983 return (nw_check(cls
, context
)
984 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
985 'TextureNodeTree', 'GeometryNodeTree'})
986 and nw_check_selected(cls
, context
))
988 def execute(self
, context
):
989 settings
= context
.preferences
.addons
[__package__
].preferences
990 merge_hide
= settings
.merge_hide
991 merge_position
= settings
.merge_position
# 'center' or 'bottom'
994 do_hide_shader
= False
995 if merge_hide
== 'ALWAYS':
997 do_hide_shader
= True
998 elif merge_hide
== 'NON_SHADER':
1001 tree_type
= context
.space_data
.node_tree
.type
1002 if tree_type
== 'GEOMETRY':
1003 node_type
= 'GeometryNode'
1004 if tree_type
== 'COMPOSITING':
1005 node_type
= 'CompositorNode'
1006 elif tree_type
== 'SHADER':
1007 node_type
= 'ShaderNode'
1008 elif tree_type
== 'TEXTURE':
1009 node_type
= 'TextureNode'
1010 nodes
, links
= get_nodes_links(context
)
1012 merge_type
= self
.merge_type
1013 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1014 # 'ZCOMBINE' works only if mode == 'MIX'
1015 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1016 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
1019 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
1021 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1022 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
1023 node_type
= 'ShaderNode'
1024 selected_mix
= [] # entry = [index, loc]
1025 selected_shader
= [] # entry = [index, loc]
1026 selected_geometry
= [] # entry = [index, loc]
1027 selected_math
= [] # entry = [index, loc]
1028 selected_vector
= [] # entry = [index, loc]
1029 selected_z
= [] # entry = [index, loc]
1030 selected_alphaover
= [] # entry = [index, loc]
1032 for i
, node
in enumerate(nodes
):
1033 if node
.select
and node
.outputs
:
1034 if merge_type
== 'AUTO':
1035 for (type, types_list
, dst
) in (
1036 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1037 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1038 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
1039 ('VALUE', [t
[0] for t
in operations
], selected_math
),
1040 ('VECTOR', [], selected_vector
),
1042 output
= get_first_enabled_output(node
)
1043 output_type
= output
.type
1044 valid_mode
= mode
in types_list
1045 # When mode is 'MIX' we have to cheat since the mix node is not used in
1047 if tree_type
== 'GEOMETRY':
1049 if output_type
== 'VALUE' and type == 'VALUE':
1051 elif output_type
== 'VECTOR' and type == 'VECTOR':
1053 elif type == 'GEOMETRY':
1055 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1056 # Cheat that output type is 'RGBA',
1057 # and that 'MIX' exists in math operations list.
1058 # This way when selected_mix list is analyzed:
1059 # Node data will be appended even though it doesn't meet requirements.
1060 elif output_type
!= 'SHADER' and mode
== 'MIX':
1061 output_type
= 'RGBA'
1063 if output_type
== type and valid_mode
:
1064 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1066 for (type, types_list
, dst
) in (
1067 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1068 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1069 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1070 ('MATH', [t
[0] for t
in operations
], selected_math
),
1071 ('ZCOMBINE', ('MIX', ), selected_z
),
1072 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1074 if merge_type
== type and mode
in types_list
:
1075 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1076 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1077 # use only 'Mix' nodes for merging.
1078 # For that we add selected_math list to selected_mix list and clear selected_math.
1079 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1080 selected_mix
+= selected_math
1083 # If no nodes are selected, do nothing and pass through.
1084 if not (selected_mix
+ selected_shader
+ selected_geometry
+ selected_math
1085 + selected_vector
+ selected_z
+ selected_alphaover
):
1086 return {'PASS_THROUGH'}
1095 selected_alphaover
]:
1098 count_before
= len(nodes
)
1099 # sort list by loc_x - reversed
1100 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1102 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1103 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1105 # Change the node type for math nodes in a geometry node tree.
1106 if tree_type
== 'GEOMETRY':
1107 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1108 node_type
= 'ShaderNode'
1112 node_type
= 'GeometryNode'
1113 if merge_position
== 'CENTER':
1114 # average yloc of last two nodes (lowest two)
1115 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1116 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1122 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1126 if nodes_list
== selected_shader
and not do_hide_shader
:
1128 the_range
= len(nodes_list
) - 1
1129 if len(nodes_list
) == 1:
1132 for i
in range(the_range
):
1133 if nodes_list
== selected_mix
:
1135 if tree_type
== 'COMPOSITING':
1137 add_type
= node_type
+ mix_name
1138 add
= nodes
.new(add_type
)
1139 if tree_type
!= 'COMPOSITING':
1140 add
.data_type
= 'RGBA'
1141 add
.blend_type
= mode
1143 add
.inputs
[0].default_value
= 1.0
1144 add
.show_preview
= False
1150 if tree_type
== 'COMPOSITING':
1153 elif nodes_list
== selected_math
:
1154 add_type
= node_type
+ 'Math'
1155 add
= nodes
.new(add_type
)
1156 add
.operation
= mode
1162 elif nodes_list
== selected_shader
:
1164 add_type
= node_type
+ 'MixShader'
1165 add
= nodes
.new(add_type
)
1166 add
.hide
= do_hide_shader
1172 add_type
= node_type
+ 'AddShader'
1173 add
= nodes
.new(add_type
)
1174 add
.hide
= do_hide_shader
1179 elif nodes_list
== selected_geometry
:
1180 if mode
in ('JOIN', 'MIX'):
1181 add_type
= node_type
+ 'JoinGeometry'
1182 add
= self
.merge_with_multi_input(
1183 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1185 add_type
= node_type
+ 'MeshBoolean'
1186 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1187 add
= self
.merge_with_multi_input(
1188 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1189 add
.operation
= mode
1192 elif nodes_list
== selected_vector
:
1193 add_type
= node_type
+ 'VectorMath'
1194 add
= nodes
.new(add_type
)
1195 add
.operation
= mode
1201 elif nodes_list
== selected_z
:
1202 add
= nodes
.new('CompositorNodeZcombine')
1203 add
.show_preview
= False
1209 elif nodes_list
== selected_alphaover
:
1210 add
= nodes
.new('CompositorNodeAlphaOver')
1211 add
.show_preview
= False
1217 add
.location
= loc_x
, loc_y
1221 # This has already been handled separately
1225 count_after
= len(nodes
)
1226 index
= count_after
- 1
1227 first_selected
= nodes
[nodes_list
[0][0]]
1228 # "last" node has been added as first, so its index is count_before.
1229 last_add
= nodes
[count_before
]
1230 # Create list of invalid indexes.
1231 invalid_nodes
= [nodes
[n
[0]]
1232 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1235 # Two nodes were selected and first selected has no output links, second selected has output links.
1236 # Then add links from last add to all links 'to_socket' of out links of second selected.
1237 first_selected_output
= get_first_enabled_output(first_selected
)
1238 if len(nodes_list
) == 2:
1239 if not first_selected_output
.links
:
1240 second_selected
= nodes
[nodes_list
[1][0]]
1241 for ss_link
in get_first_enabled_output(second_selected
).links
:
1242 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1243 # Link only if "to_node" index not in invalid indexes list.
1244 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1245 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1246 # add links from last_add to all links 'to_socket' of out links of first selected.
1247 for fs_link
in first_selected_output
.links
:
1248 # Link only if "to_node" index not in invalid indexes list.
1249 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1250 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1251 # add link from "first" selected and "first" add node
1252 node_to
= nodes
[count_after
- 1]
1253 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1254 if node_to
.type == 'ZCOMBINE':
1255 for fs_out
in first_selected
.outputs
:
1256 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1257 connect_sockets(fs_out
, node_to
.inputs
[1])
1259 # add links between added ADD nodes and between selected and ADD nodes
1260 for i
in range(count_adds
):
1261 if i
< count_adds
- 1:
1262 node_from
= nodes
[index
]
1263 node_to
= nodes
[index
- 1]
1264 node_to_input_i
= first
1265 node_to_z_i
= 1 # if z combine - link z to first z input
1266 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1267 if node_to
.type == 'ZCOMBINE':
1268 for from_out
in node_from
.outputs
:
1269 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1270 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1271 if len(nodes_list
) > 1:
1272 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1273 node_to
= nodes
[index
]
1274 node_to_input_i
= second
1275 node_to_z_i
= 3 # if z combine - link z to second 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
])
1282 # set "last" of added nodes as active
1283 nodes
.active
= last_add
1284 for i
, x
, y
, dx
, h
in nodes_list
:
1285 nodes
[i
].select
= False
1290 class NWBatchChangeNodes(Operator
, NWBase
):
1291 bl_idname
= "node.nw_batch_change"
1292 bl_label
= "Batch Change"
1293 bl_description
= "Batch Change Blend Type and Math Operation"
1294 bl_options
= {'REGISTER', 'UNDO'}
1296 blend_type
: EnumProperty(
1298 items
=blend_types
+ navs
,
1300 operation
: EnumProperty(
1302 items
=operations
+ navs
,
1306 def poll(cls
, context
):
1307 return (nw_check(cls
, context
)
1308 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
1309 'TextureNodeTree', 'GeometryNodeTree'})
1310 and nw_check_selected(cls
, context
))
1312 def execute(self
, context
):
1313 blend_type
= self
.blend_type
1314 operation
= self
.operation
1315 for node
in context
.selected_nodes
:
1316 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1317 if blend_type
not in [nav
[0] for nav
in navs
]:
1318 node
.blend_type
= blend_type
1320 if blend_type
== 'NEXT':
1321 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1322 # index = blend_types.index(node.blend_type)
1323 if index
== len(blend_types
) - 1:
1324 node
.blend_type
= blend_types
[0][0]
1326 node
.blend_type
= blend_types
[index
+ 1][0]
1328 if blend_type
== 'PREV':
1329 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1331 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1333 node
.blend_type
= blend_types
[index
- 1][0]
1335 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1336 if operation
not in [nav
[0] for nav
in navs
]:
1337 node
.operation
= operation
1339 if operation
== 'NEXT':
1340 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1341 # index = operations.index(node.operation)
1342 if index
== len(operations
) - 1:
1343 node
.operation
= operations
[0][0]
1345 node
.operation
= operations
[index
+ 1][0]
1347 if operation
== 'PREV':
1348 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1349 # index = operations.index(node.operation)
1351 node
.operation
= operations
[len(operations
) - 1][0]
1353 node
.operation
= operations
[index
- 1][0]
1358 class NWChangeMixFactor(Operator
, NWBase
):
1359 bl_idname
= "node.nw_factor"
1360 bl_label
= "Change Factor"
1361 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1362 bl_options
= {'REGISTER', 'UNDO'}
1365 def poll(cls
, context
):
1366 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
1368 # option: Change factor.
1369 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1370 # Else - change factor by option value.
1371 option
: FloatProperty()
1373 def execute(self
, context
):
1374 nodes
, links
= get_nodes_links(context
)
1375 option
= self
.option
1376 selected
= [] # entry = index
1377 for si
, node
in enumerate(nodes
):
1379 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1383 fac
= nodes
[si
].inputs
[0]
1384 nodes
[si
].hide
= False
1385 if option
in {0.0, 1.0}:
1386 fac
.default_value
= option
1388 fac
.default_value
+= option
1393 class NWCopySettings(Operator
, NWBase
):
1394 bl_idname
= "node.nw_copy_settings"
1395 bl_label
= "Copy Settings"
1396 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1397 bl_options
= {'REGISTER', 'UNDO'}
1400 def poll(cls
, context
):
1401 return (nw_check(cls
, context
)
1402 and nw_check_active(cls
, context
)
1403 and nw_check_selected(cls
, context
, min=2)
1404 and nw_check_node_type(cls
, context
, 'FRAME', invert
=True))
1406 def execute(self
, context
):
1407 node_active
= context
.active_node
1408 node_selected
= context
.selected_nodes
1409 selected_node_names
= [n
.name
for n
in node_selected
]
1411 # Get nodes in selection by type
1412 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1414 if not (len(valid_nodes
) > 1) and node_active
:
1415 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1416 return {'CANCELLED'}
1418 if len(valid_nodes
) != len(node_selected
):
1419 # Report nodes that are not valid
1420 valid_node_names
= [n
.name
for n
in valid_nodes
]
1421 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1424 "Ignored {} (not of the same type as {})".format(
1425 ", ".join(not_valid_names
),
1428 # Reference original
1430 # node_selected_names = [n.name for n in node_selected]
1435 # Deselect all nodes
1436 for i
in node_selected
:
1439 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1440 # Run through all other nodes
1441 for node
in valid_nodes
[1:]:
1443 # Check for frame node
1444 parent
= node
.parent
if node
.parent
else None
1445 node_loc
= [node
.location
.x
, node
.location
.y
]
1447 # Select original to duplicate
1450 # Duplicate selected node
1451 bpy
.ops
.node
.duplicate()
1452 new_node
= context
.selected_nodes
[0]
1455 new_node
.select
= False
1457 # Properties to copy
1458 node_tree
= node
.id_data
1459 props_to_copy
= 'bl_idname name location height width'.split(' ')
1463 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1464 for i
in (i
for i
in mappings
if i
.is_linked
):
1466 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1469 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1470 props_to_copy
.pop(0)
1472 for prop
in props_to_copy
:
1473 setattr(new_node
, prop
, props
[prop
])
1475 # Get the node tree to remove the old node
1476 nodes
= node_tree
.nodes
1478 new_node
.name
= props
['name']
1481 new_node
.parent
= parent
1482 new_node
.location
= node_loc
1484 for str_from
, str_to
in reconnections
:
1485 connect_sockets(eval(str_from
), eval(str_to
))
1487 success_names
.append(new_node
.name
)
1490 node_tree
.nodes
.active
= orig
1493 "Successfully copied attributes from {} to: {}".format(
1495 ", ".join(success_names
)))
1499 class NWCopyLabel(Operator
, NWBase
):
1500 bl_idname
= "node.nw_copy_label"
1501 bl_label
= "Copy Label"
1502 bl_options
= {'REGISTER', 'UNDO'}
1503 bl_description
= "Copy label from active to selected nodes"
1505 option
: EnumProperty(
1507 description
="Source of name of label",
1509 ('FROM_ACTIVE', 'from active', 'from active node',),
1510 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1511 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1516 def poll(cls
, context
):
1517 return nw_check(cls
, context
) and nw_check_selected(cls
, context
, min=2)
1519 def execute(self
, context
):
1520 nodes
, links
= get_nodes_links(context
)
1521 option
= self
.option
1522 active
= nodes
.active
1523 if option
== 'FROM_ACTIVE':
1525 src_label
= active
.label
1526 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1527 node
.label
= src_label
1528 elif option
== 'FROM_NODE':
1529 selected
= [n
for n
in nodes
if n
.select
]
1530 for node
in selected
:
1531 for input in node
.inputs
:
1533 src
= input.links
[0].from_node
1534 node
.label
= src
.label
1536 elif option
== 'FROM_SOCKET':
1537 selected
= [n
for n
in nodes
if n
.select
]
1538 for node
in selected
:
1539 for input in node
.inputs
:
1541 src
= input.links
[0].from_socket
1542 node
.label
= src
.name
1548 class NWClearLabel(Operator
, NWBase
):
1549 bl_idname
= "node.nw_clear_label"
1550 bl_label
= "Clear Label"
1551 bl_options
= {'REGISTER', 'UNDO'}
1552 bl_description
= "Clear labels on selected nodes"
1554 option
: BoolProperty()
1557 def poll(cls
, context
):
1558 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
1560 def execute(self
, context
):
1561 nodes
, links
= get_nodes_links(context
)
1562 for node
in [n
for n
in nodes
if n
.select
]:
1567 def invoke(self
, context
, event
):
1569 return self
.execute(context
)
1571 return context
.window_manager
.invoke_confirm(self
, event
)
1574 class NWModifyLabels(Operator
, NWBase
):
1575 """Modify labels of all selected nodes"""
1576 bl_idname
= "node.nw_modify_labels"
1577 bl_label
= "Modify Labels"
1578 bl_options
= {'REGISTER', 'UNDO'}
1580 prepend
: StringProperty(
1581 name
="Add to Beginning"
1583 append
: StringProperty(
1586 replace_from
: StringProperty(
1587 name
="Text to Replace"
1589 replace_to
: StringProperty(
1594 def poll(cls
, context
):
1595 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
1597 def execute(self
, context
):
1598 nodes
, links
= get_nodes_links(context
)
1599 for node
in [n
for n
in nodes
if n
.select
]:
1600 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1604 def invoke(self
, context
, event
):
1608 return context
.window_manager
.invoke_props_dialog(self
)
1611 class NWAddTextureSetup(Operator
, NWBase
):
1612 bl_idname
= "node.nw_add_texture"
1613 bl_label
= "Texture Setup"
1614 bl_description
= "Add Texture Node Setup to Selected Shaders"
1615 bl_options
= {'REGISTER', 'UNDO'}
1617 add_mapping
: BoolProperty(
1618 name
="Add Mapping Nodes",
1619 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1623 def poll(cls
, context
):
1624 return (nw_check(cls
, context
)
1625 and nw_check_space_type(cls
, context
, {'ShaderNodeTree'})
1626 and nw_check_selected(cls
, context
))
1628 def execute(self
, context
):
1629 nodes
, links
= get_nodes_links(context
)
1631 texture_types
= get_texture_node_types()
1632 selected_nodes
= [n
for n
in nodes
if n
.select
]
1634 for node
in selected_nodes
:
1639 target_input
= node
.inputs
[0]
1640 for input in node
.inputs
:
1643 if not input.is_linked
:
1644 target_input
= input
1647 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1652 locx
= node
.location
.x
1653 locy
= node
.location
.y
- (input_index
* padding
)
1655 is_texture_node
= node
.rna_type
.identifier
in texture_types
1656 use_environment_texture
= node
.type == 'BACKGROUND'
1658 # Add an image texture before normal shader nodes.
1659 if not is_texture_node
:
1660 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1661 image_texture_node
= nodes
.new(image_texture_type
)
1662 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1663 image_texture_node
.location
= [locx
- x_offset
, locy
]
1664 nodes
.active
= image_texture_node
1665 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1667 # The mapping setup following this will connect to the first input of this image texture.
1668 target_input
= image_texture_node
.inputs
[0]
1672 if is_texture_node
or self
.add_mapping
:
1674 mapping_node
= nodes
.new('ShaderNodeMapping')
1675 x_offset
= x_offset
+ mapping_node
.width
+ padding
1676 mapping_node
.location
= [locx
- x_offset
, locy
]
1677 connect_sockets(mapping_node
.outputs
[0], target_input
)
1679 # Add Texture Coordinates node.
1680 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1681 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1682 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1684 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1685 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1686 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1687 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1692 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1693 bl_idname
= "node.nw_add_textures_for_principled"
1694 bl_label
= "Principled Texture Setup"
1695 bl_description
= "Add Texture Node Setup for Principled BSDF"
1696 bl_options
= {'REGISTER', 'UNDO'}
1698 directory
: StringProperty(
1702 description
='Folder to search in for image files'
1704 files
: CollectionProperty(
1705 type=bpy
.types
.OperatorFileListElement
,
1706 options
={'HIDDEN', 'SKIP_SAVE'}
1709 relative_path
: BoolProperty(
1710 name
='Relative Path',
1711 description
='Set the file path relative to the blend file, when possible',
1720 def draw(self
, context
):
1721 layout
= self
.layout
1722 layout
.alignment
= 'LEFT'
1724 layout
.prop(self
, 'relative_path')
1727 def poll(cls
, context
):
1728 return (nw_check(cls
, context
)
1729 and nw_check_active(cls
, context
)
1730 and nw_check_space_type(cls
, context
, {'ShaderNodeTree'})
1731 and nw_check_node_type(cls
, context
, 'BSDF_PRINCIPLED'))
1733 def execute(self
, context
):
1734 # Check if everything is ok
1735 if not self
.directory
:
1736 self
.report({'INFO'}, 'No folder selected')
1737 return {'CANCELLED'}
1738 if not self
.files
[:]:
1739 self
.report({'INFO'}, 'No files selected')
1740 return {'CANCELLED'}
1742 nodes
, links
= get_nodes_links(context
)
1743 active_node
= nodes
.active
1745 # Filter textures names for texturetypes in filenames
1746 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1747 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1748 normal_abbr
= tags
.normal
.split(' ')
1749 bump_abbr
= tags
.bump
.split(' ')
1750 gloss_abbr
= tags
.gloss
.split(' ')
1751 rough_abbr
= tags
.rough
.split(' ')
1753 ['Displacement', tags
.displacement
.split(' '), None],
1754 ['Base Color', tags
.base_color
.split(' '), None],
1755 ['Metallic', tags
.metallic
.split(' '), None],
1756 ['Specular IOR Level', tags
.specular
.split(' '), None],
1757 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1758 ['Bump', bump_abbr
, None],
1759 ['Normal', normal_abbr
, None],
1760 ['Transmission Weight', tags
.transmission
.split(' '), None],
1761 ['Emission Color', tags
.emission
.split(' '), None],
1762 ['Alpha', tags
.alpha
.split(' '), None],
1763 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1766 match_files_to_socket_names(self
.files
, socketnames
)
1767 # Remove socketnames without found files
1768 socketnames
= [s
for s
in socketnames
if s
[2]
1769 and path
.exists(self
.directory
+ s
[2])]
1771 self
.report({'INFO'}, 'No matching images found')
1772 print('No matching images found')
1773 return {'CANCELLED'}
1775 # Don't override path earlier as os.path is used to check the absolute path
1776 import_path
= self
.directory
1777 if self
.relative_path
:
1778 if bpy
.data
.filepath
:
1780 import_path
= bpy
.path
.relpath(self
.directory
)
1785 print('\nMatched Textures:')
1790 normal_node_texture
= None
1792 bump_node_texture
= None
1793 roughness_node
= None
1794 for i
, sname
in enumerate(socketnames
):
1795 print(i
, sname
[0], sname
[2])
1797 # DISPLACEMENT NODES
1798 if sname
[0] == 'Displacement':
1799 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1800 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1801 disp_texture
.image
= img
1802 disp_texture
.label
= 'Displacement'
1803 if disp_texture
.image
:
1804 disp_texture
.image
.colorspace_settings
.is_data
= True
1806 # Add displacement offset nodes
1807 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1808 # Align the Displacement node under the active Principled BSDF node
1809 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1810 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1812 # TODO Turn on true displacement in the material
1813 # Too complicated for now
1816 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1818 if not output_node
[0].inputs
[2].is_linked
:
1819 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1824 elif sname
[0] == 'Bump':
1825 # Test if new texture node is bump map
1826 fname_components
= split_into_components(sname
[2])
1827 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1829 # If Bump add bump node in between
1830 bump_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1831 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1832 bump_node_texture
.image
= img
1833 bump_node_texture
.label
= 'Bump'
1836 bump_node
= nodes
.new(type='ShaderNodeBump')
1837 link
= connect_sockets(bump_node
.inputs
[2], bump_node_texture
.outputs
[0])
1838 link
= connect_sockets(active_node
.inputs
['Normal'], bump_node
.outputs
[0])
1842 elif sname
[0] == 'Normal':
1843 # Test if new texture node is normal map
1844 fname_components
= split_into_components(sname
[2])
1845 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1847 # If Normal add normal node in between
1848 normal_node_texture
= nodes
.new(type='ShaderNodeTexImage')
1849 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1850 normal_node_texture
.image
= img
1851 normal_node_texture
.label
= 'Normal'
1854 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1855 link
= connect_sockets(normal_node
.inputs
[1], normal_node_texture
.outputs
[0])
1856 # Connect to bump node if it was created before, otherwise to the BSDF
1857 if bump_node
is None:
1858 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1860 link
= connect_sockets(bump_node
.inputs
[sname
[0]], normal_node
.outputs
[sname
[0]])
1863 # AMBIENT OCCLUSION TEXTURE
1864 elif sname
[0] == 'Ambient Occlusion':
1865 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1866 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1867 ao_texture
.image
= img
1868 ao_texture
.label
= sname
[0]
1869 if ao_texture
.image
:
1870 ao_texture
.image
.colorspace_settings
.is_data
= True
1874 if not active_node
.inputs
[sname
[0]].is_linked
:
1875 # No texture node connected -> add texture node with new image
1876 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1877 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1878 texture_node
.image
= img
1880 if sname
[0] == 'Roughness':
1881 # Test if glossy or roughness map
1882 fname_components
= split_into_components(sname
[2])
1883 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1884 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1887 # If Roughness nothing to to
1888 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1891 # If Gloss Map add invert node
1892 invert_node
= nodes
.new(type='ShaderNodeInvert')
1893 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1895 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1896 roughness_node
= texture_node
1899 # This is a simple connection Texture --> Input slot
1900 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1902 # Use non-color except for color inputs
1903 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1904 texture_node
.image
.colorspace_settings
.is_data
= True
1907 # If already texture connected. add to node list for alignment
1908 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1910 # This are all connected texture nodes
1911 texture_nodes
.append(texture_node
)
1912 texture_node
.label
= sname
[0]
1915 texture_nodes
.append(disp_texture
)
1916 if bump_node_texture
:
1917 texture_nodes
.append(bump_node_texture
)
1918 if normal_node_texture
:
1919 texture_nodes
.append(normal_node_texture
)
1922 # We want the ambient occlusion texture to be the top most texture node
1923 texture_nodes
.insert(0, ao_texture
)
1926 for i
, texture_node
in enumerate(texture_nodes
):
1927 offset
= Vector((-550, (i
* -280) + 200))
1928 texture_node
.location
= active_node
.location
+ offset
1931 # Extra alignment if normal node was added
1932 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1935 # Extra alignment if bump node was added
1936 bump_node
.location
= bump_node_texture
.location
+ Vector((300, 0))
1939 # Alignment of invert node if glossy map
1940 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1942 # Add texture input + mapping
1943 mapping
= nodes
.new(type='ShaderNodeMapping')
1944 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1945 if len(texture_nodes
) > 1:
1946 # If more than one texture add reroute node in between
1947 reroute
= nodes
.new(type='NodeReroute')
1948 texture_nodes
.append(reroute
)
1949 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1950 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1951 reroute
.location
= tex_coords
+ Vector((-50, -120))
1952 for texture_node
in texture_nodes
:
1953 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1954 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1956 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1958 # Connect texture_coordinates to mapping node
1959 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1960 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1961 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1963 # Create frame around tex coords and mapping
1964 frame
= nodes
.new(type='NodeFrame')
1965 frame
.label
= 'Mapping'
1966 mapping
.parent
= frame
1967 texture_input
.parent
= frame
1970 # Create frame around texture nodes
1971 frame
= nodes
.new(type='NodeFrame')
1972 frame
.label
= 'Textures'
1973 for tnode
in texture_nodes
:
1974 tnode
.parent
= frame
1978 active_node
.select
= False
1981 force_update(context
)
1985 class NWAddReroutes(Operator
, NWBase
):
1986 """Add Reroute Nodes and link them to outputs of selected nodes"""
1987 bl_idname
= "node.nw_add_reroutes"
1988 bl_label
= "Add Reroutes"
1989 bl_description
= "Add Reroutes to Outputs"
1990 bl_options
= {'REGISTER', 'UNDO'}
1992 option
: EnumProperty(
1995 ('ALL', 'to all', 'Add to all outputs'),
1996 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1997 ('LINKED', 'to linked', 'Add only to linked outputs'),
2002 def poll(cls
, context
):
2003 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
2005 def execute(self
, context
):
2006 nodes
, _links
= get_nodes_links(context
)
2007 post_select
= [] # Nodes to be selected after execution.
2010 # Create reroutes and recreate links.
2011 for node
in [n
for n
in nodes
if n
.select
]:
2012 if not node
.outputs
:
2014 x
= node
.location
.x
+ node
.width
+ 20.0
2016 new_node_reroutes
= []
2018 # Unhide 'REROUTE' nodes to avoid issues with location.y
2019 if node
.type == 'REROUTE':
2024 reroutes_count
= 0 # Will be used when aligning reroutes added to hidden nodes.
2025 for out_i
, output
in enumerate(node
.outputs
):
2026 if output
.is_unavailable
:
2028 if node
.type == 'R_LAYERS' and output
.name
!= 'Alpha':
2029 # If 'R_LAYERS' check if output is used in render pass.
2030 # If output is "Alpha", assume it's used. Not available in passes.
2031 node_scene
= node
.scene
2032 node_layer
= node
.layer
2033 for rlo
in rl_outputs
:
2034 # Check entries in global 'rl_outputs' variable.
2035 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2036 if not getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
):
2038 # Output is valid when option is 'all' or when 'loose' output has no links.
2039 valid
= ((self
.option
== 'ALL') or
2040 (self
.option
== 'LOOSE' and not output
.links
) or
2041 (self
.option
== 'LINKED' and output
.links
))
2043 # Add reroutes only if valid.
2044 n
= nodes
.new('NodeReroute')
2046 for link
in output
.links
:
2047 connect_sockets(n
.outputs
[0], link
.to_socket
)
2048 connect_sockets(output
, n
.inputs
[0])
2050 new_node_reroutes
.append(n
)
2051 post_select
.append(n
)
2052 if valid
or not output
.hide
:
2053 # Offset reroutes for all outputs, except hidden ones.
2057 # Nicer reroutes distribution along y when node.hide.
2059 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2060 for reroute
in new_node_reroutes
:
2061 reroute
.location
.y
-= y_translate
2065 # Select only newly created nodes.
2066 node
.select
= node
in post_select
2068 # No new nodes were created.
2069 return {'CANCELLED'}
2074 class NWLinkActiveToSelected(Operator
, NWBase
):
2075 """Link active node to selected nodes basing on various criteria"""
2076 bl_idname
= "node.nw_link_active_to_selected"
2077 bl_label
= "Link Active Node to Selected"
2078 bl_options
= {'REGISTER', 'UNDO'}
2080 replace
: BoolProperty()
2081 use_node_name
: BoolProperty()
2082 use_outputs_names
: BoolProperty()
2085 def poll(cls
, context
):
2086 return (nw_check(cls
, context
)
2087 and nw_check_active(cls
, context
)
2088 and nw_check_selected(cls
, context
, min=2))
2090 def execute(self
, context
):
2091 nodes
, links
= get_nodes_links(context
)
2092 replace
= self
.replace
2093 use_node_name
= self
.use_node_name
2094 use_outputs_names
= self
.use_outputs_names
2095 active
= nodes
.active
2096 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2097 outputs
= [] # Only usable outputs of active nodes will be stored here.
2098 for out
in active
.outputs
:
2099 if active
.type != 'R_LAYERS':
2102 # 'R_LAYERS' node type needs special handling.
2103 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2104 # Only outputs that represent used passes should be taken into account
2105 # Check if pass represented by output is used.
2106 # global 'rl_outputs' list will be used for that
2107 for rlo
in rl_outputs
:
2108 pass_used
= False # initial value. Will be set to True if pass is used
2109 if out
.name
== 'Alpha':
2110 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2112 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2113 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2114 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2118 doit
= True # Will be changed to False when links successfully added to previous output.
2121 for node
in selected
:
2122 dst_name
= node
.name
# Will be compared with src_name if needed.
2123 # When node has label - use it as dst_name
2125 dst_name
= node
.label
2126 valid
= True # Initial value. Will be changed to False if names don't match.
2127 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2129 # Set src_name to source node name or label
2130 src_name
= active
.name
2132 src_name
= active
.label
2133 elif use_outputs_names
:
2134 src_name
= (out
.name
, )
2135 for rlo
in rl_outputs
:
2136 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2137 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2138 if dst_name
not in src_name
:
2141 for input in node
.inputs
:
2142 if input.type == out
.type or node
.type == 'REROUTE':
2143 if replace
or not input.is_linked
:
2144 connect_sockets(out
, input)
2145 if not use_node_name
and not use_outputs_names
:
2152 class NWAlignNodes(Operator
, NWBase
):
2153 '''Align the selected nodes neatly in a row/column'''
2154 bl_idname
= "node.nw_align_nodes"
2155 bl_label
= "Align Nodes"
2156 bl_options
= {'REGISTER', 'UNDO'}
2157 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2160 def poll(cls
, context
):
2161 return nw_check(cls
, context
) and nw_check_not_empty(cls
, context
)
2163 def execute(self
, context
):
2164 nodes
, links
= get_nodes_links(context
)
2165 margin
= self
.margin
2169 if node
.select
and node
.type != 'FRAME':
2170 selection
.append(node
)
2172 # If no nodes are selected, align all nodes
2176 elif nodes
.active
in selection
:
2177 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2179 # Check if nodes should be laid out horizontally or vertically
2180 # use dimension to get center of node, not corner
2181 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2182 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2183 x_range
= max(x_locs
) - min(x_locs
)
2184 y_range
= max(y_locs
) - min(y_locs
)
2185 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2186 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2187 horizontal
= x_range
> y_range
2189 # Sort selection by location of node mid-point
2191 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2193 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2197 for node
in selection
:
2198 current_margin
= margin
2199 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2202 node
.location
.x
= current_pos
2203 current_pos
+= current_margin
+ node
.dimensions
.x
2204 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2206 node
.location
.y
= current_pos
2207 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2208 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2210 # If active node is selected, center nodes around it
2211 if active_loc
is not None:
2212 active_loc_diff
= active_loc
- nodes
.active
.location
2213 for node
in selection
:
2214 node
.location
+= active_loc_diff
2215 else: # Position nodes centered around where they used to be
2216 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2217 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2218 new_mid
= (max(locs
) + min(locs
)) / 2
2219 for node
in selection
:
2221 node
.location
.x
+= (mid_x
- new_mid
)
2223 node
.location
.y
+= (mid_y
- new_mid
)
2228 class NWSelectParentChildren(Operator
, NWBase
):
2229 bl_idname
= "node.nw_select_parent_child"
2230 bl_label
= "Select Parent or Children"
2231 bl_options
= {'REGISTER', 'UNDO'}
2233 option
: EnumProperty(
2236 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2237 ('CHILD', 'Select Children', 'Select members of selected frame'),
2242 def poll(cls
, context
):
2243 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
2245 def execute(self
, context
):
2246 nodes
, links
= get_nodes_links(context
)
2247 option
= self
.option
2248 selected
= [node
for node
in nodes
if node
.select
]
2249 if option
== 'PARENT':
2250 for sel
in selected
:
2253 parent
.select
= True
2254 else: # option == 'CHILD'
2255 for sel
in selected
:
2256 children
= [node
for node
in nodes
if node
.parent
== sel
]
2257 for kid
in children
:
2263 class NWDetachOutputs(Operator
, NWBase
):
2264 """Detach outputs of selected node leaving inputs linked"""
2265 bl_idname
= "node.nw_detach_outputs"
2266 bl_label
= "Detach Outputs"
2267 bl_options
= {'REGISTER', 'UNDO'}
2270 def poll(cls
, context
):
2271 return nw_check(cls
, context
) and nw_check_selected(cls
, context
)
2273 def execute(self
, context
):
2274 nodes
, links
= get_nodes_links(context
)
2275 selected
= context
.selected_nodes
2276 bpy
.ops
.node
.duplicate_move_keep_inputs()
2277 new_nodes
= context
.selected_nodes
2278 bpy
.ops
.node
.select_all(action
="DESELECT")
2279 for node
in selected
:
2281 bpy
.ops
.node
.delete_reconnect()
2282 for new_node
in new_nodes
:
2283 new_node
.select
= True
2284 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2289 class NWLinkToOutputNode(Operator
):
2290 """Link to Composite node or Material Output node"""
2291 bl_idname
= "node.nw_link_out"
2292 bl_label
= "Connect to Output"
2293 bl_options
= {'REGISTER', 'UNDO'}
2296 def poll(cls
, context
):
2297 """Disabled for custom nodes as we do not know which nodes are outputs."""
2298 return (nw_check(cls
, context
)
2299 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree',
2300 'TextureNodeTree', 'GeometryNodeTree'})
2301 and nw_check_active(cls
, context
)
2302 and nw_check_visible_outputs(cls
, context
))
2304 def execute(self
, context
):
2305 nodes
, links
= get_nodes_links(context
)
2306 active
= nodes
.active
2308 tree_type
= context
.space_data
.tree_type
2309 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2310 'WORLD': 'ShaderNodeOutputWorld',
2311 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2313 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2314 'CompositorNodeTree': 'CompositorNodeComposite',
2315 'TextureNodeTree': 'TextureNodeOutput',
2316 'GeometryNodeTree': 'NodeGroupOutput',
2319 # check whether the node is an output node and,
2320 # if supported, whether it's the active one
2321 if node
.rna_type
.identifier
== output_type \
2322 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2326 else: # No output node exists
2327 bpy
.ops
.node
.select_all(action
="DESELECT")
2328 output_node
= nodes
.new(output_type
)
2329 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2330 output_node
.location
.y
= active
.location
.y
2333 for i
, output
in enumerate(active
.outputs
):
2334 if is_visible_socket(output
):
2337 for i
, output
in enumerate(active
.outputs
):
2338 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2343 if tree_type
== 'ShaderNodeTree':
2344 if active
.outputs
[output_index
].name
== 'Volume':
2346 elif active
.outputs
[output_index
].name
== 'Displacement':
2348 elif tree_type
== 'GeometryNodeTree':
2349 if active
.outputs
[output_index
].type != 'GEOMETRY':
2350 return {'CANCELLED'}
2351 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2353 force_update(context
) # viewport render does not update
2358 class NWMakeLink(Operator
, NWBase
):
2359 """Make a link from one socket to another"""
2360 bl_idname
= 'node.nw_make_link'
2361 bl_label
= 'Make Link'
2362 bl_options
= {'REGISTER', 'UNDO'}
2363 from_socket
: IntProperty()
2364 to_socket
: IntProperty()
2366 def execute(self
, context
):
2367 nodes
, links
= get_nodes_links(context
)
2369 n1
= nodes
[context
.scene
.NWLazySource
]
2370 n2
= nodes
[context
.scene
.NWLazyTarget
]
2372 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2374 force_update(context
)
2379 class NWCallInputsMenu(Operator
, NWBase
):
2380 """Link from this output"""
2381 bl_idname
= 'node.nw_call_inputs_menu'
2382 bl_label
= 'Make Link'
2383 bl_options
= {'REGISTER', 'UNDO'}
2384 from_socket
: IntProperty()
2386 def execute(self
, context
):
2387 nodes
, links
= get_nodes_links(context
)
2389 context
.scene
.NWSourceSocket
= self
.from_socket
2391 n1
= nodes
[context
.scene
.NWLazySource
]
2392 n2
= nodes
[context
.scene
.NWLazyTarget
]
2393 if len(n2
.inputs
) > 1:
2394 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2395 elif len(n2
.inputs
) == 1:
2396 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2400 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2401 """Add an Image Sequence"""
2402 bl_idname
= 'node.nw_add_sequence'
2403 bl_label
= 'Import Image Sequence'
2404 bl_options
= {'REGISTER', 'UNDO'}
2406 directory
: StringProperty(
2409 filename
: StringProperty(
2412 files
: CollectionProperty(
2413 type=bpy
.types
.OperatorFileListElement
,
2414 options
={'HIDDEN', 'SKIP_SAVE'}
2416 relative_path
: BoolProperty(
2417 name
='Relative Path',
2418 description
='Set the file path relative to the blend file, when possible',
2423 def poll(cls
, context
):
2424 return (nw_check(cls
, context
)
2425 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree'}))
2427 def draw(self
, context
):
2428 layout
= self
.layout
2429 layout
.alignment
= 'LEFT'
2431 layout
.prop(self
, 'relative_path')
2433 def execute(self
, context
):
2434 nodes
, links
= get_nodes_links(context
)
2435 directory
= self
.directory
2436 filename
= self
.filename
2438 tree
= context
.space_data
.node_tree
2441 # print ("\nDIR:", directory)
2442 # print ("FN:", filename)
2443 # print ("Fs:", list(f.name for f in files), '\n')
2445 if tree
.type == 'SHADER':
2446 node_type
= "ShaderNodeTexImage"
2447 elif tree
.type == 'COMPOSITING':
2448 node_type
= "CompositorNodeImage"
2450 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2451 return {'CANCELLED'}
2453 if not files
[0].name
and not filename
:
2454 self
.report({'ERROR'}, "No file chosen")
2455 return {'CANCELLED'}
2456 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2457 # User has selected multiple files without an active one, or the active one is non-existent
2458 filename
= files
[0].name
2460 if not path
.exists(directory
+ filename
):
2461 self
.report({'ERROR'}, filename
+ " does not exist!")
2462 return {'CANCELLED'}
2464 without_ext
= '.'.join(filename
.split('.')[:-1])
2466 # if last digit isn't a number, it's not a sequence
2467 if not without_ext
[-1].isdigit():
2468 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2469 return {'CANCELLED'}
2471 extension
= filename
.split('.')[-1]
2472 reverse
= without_ext
[::-1] # reverse string
2475 for char
in reverse
:
2481 without_num
= without_ext
[:count_numbers
* -1]
2483 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2485 num_frames
= len(files
)
2487 nodes_list
= [node
for node
in nodes
]
2489 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2490 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2494 yloc
+= node_mid_pt(node
, 'y')
2495 yloc
= yloc
/ len(nodes
)
2500 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2502 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2504 node
.label
= name_with_hashes
2506 filepath
= directory
+ (without_ext
+ '.' + extension
)
2507 if self
.relative_path
:
2508 if bpy
.data
.filepath
:
2510 filepath
= bpy
.path
.relpath(filepath
)
2514 img
= bpy
.data
.images
.load(filepath
)
2515 img
.source
= 'SEQUENCE'
2516 img
.name
= name_with_hashes
2518 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2519 # separate the number from the file name of the first file
2520 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2521 image_user
.frame_duration
= num_frames
2526 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2527 """Add multiple images at once"""
2528 bl_idname
= 'node.nw_add_multiple_images'
2529 bl_label
= 'Open Selected Images'
2530 bl_options
= {'REGISTER', 'UNDO'}
2531 directory
: StringProperty(
2534 files
: CollectionProperty(
2535 type=bpy
.types
.OperatorFileListElement
,
2536 options
={'HIDDEN', 'SKIP_SAVE'}
2540 def poll(cls
, context
):
2541 return (nw_check(cls
, context
)
2542 and nw_check_space_type(cls
, context
, {'ShaderNodeTree', 'CompositorNodeTree'}))
2544 def execute(self
, context
):
2545 nodes
, links
= get_nodes_links(context
)
2547 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2549 if context
.space_data
.node_tree
.type == 'SHADER':
2550 node_type
= "ShaderNodeTexImage"
2551 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2552 node_type
= "CompositorNodeImage"
2554 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2555 return {'CANCELLED'}
2558 for f
in self
.files
:
2561 node
= nodes
.new(node_type
)
2562 new_nodes
.append(node
)
2565 node
.location
.x
= xloc
2566 node
.location
.y
= yloc
2569 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2572 # shift new nodes up to center of tree
2573 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2575 if node
in new_nodes
:
2577 node
.location
.y
+= (list_size
/ 2)
2583 class NWViewerFocus(bpy
.types
.Operator
):
2584 """Set the viewer tile center to the mouse position"""
2585 bl_idname
= "node.nw_viewer_focus"
2586 bl_label
= "Viewer Focus"
2588 x
: bpy
.props
.IntProperty()
2589 y
: bpy
.props
.IntProperty()
2592 def poll(cls
, context
):
2593 return (nw_check(cls
, context
)
2594 and nw_check_space_type(cls
, context
, {'CompositorNodeTree'}))
2596 def execute(self
, context
):
2599 def invoke(self
, context
, event
):
2600 render
= context
.scene
.render
2601 space
= context
.space_data
2602 percent
= render
.resolution_percentage
* 0.01
2604 nodes
, links
= get_nodes_links(context
)
2605 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2608 mlocx
= event
.mouse_region_x
2609 mlocy
= event
.mouse_region_y
2610 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2612 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2613 region_x
= context
.region
.width
2614 region_y
= context
.region
.height
2616 region_center_x
= context
.region
.width
/ 2
2617 region_center_y
= context
.region
.height
/ 2
2619 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2620 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2622 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2623 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2625 margin_x
= region_center_x
- backdrop_center_x
2626 margin_y
= region_center_y
- backdrop_center_y
2628 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2629 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2631 for node
in viewers
:
2632 node
.center_x
= abs_mouse_x
2633 node
.center_y
= abs_mouse_y
2635 return {'PASS_THROUGH'}
2637 return self
.execute(context
)
2640 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2641 """Save the current viewer node to an image file"""
2642 bl_idname
= "node.nw_save_viewer"
2643 bl_label
= "Save This Image"
2644 filepath
: StringProperty(subtype
="FILE_PATH")
2645 filename_ext
: EnumProperty(
2647 description
="Choose the file format to save to",
2648 items
=(('.bmp', "BMP", ""),
2649 ('.rgb', 'IRIS', ""),
2650 ('.png', 'PNG', ""),
2651 ('.jpg', 'JPEG', ""),
2652 ('.jp2', 'JPEG2000', ""),
2653 ('.tga', 'TARGA', ""),
2654 ('.cin', 'CINEON', ""),
2655 ('.dpx', 'DPX', ""),
2656 ('.exr', 'OPEN_EXR', ""),
2657 ('.hdr', 'HDR', ""),
2658 ('.tif', 'TIFF', "")),
2663 def poll(cls
, context
):
2664 return (nw_check(cls
, context
)
2665 and nw_check_space_type(cls
, context
, {'CompositorNodeTree'})
2666 and nw_check_viewer_node(cls
))
2668 def execute(self
, context
):
2685 basename
, ext
= path
.splitext(fp
)
2686 old_render_format
= context
.scene
.render
.image_settings
.file_format
2687 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2688 context
.area
.type = "IMAGE_EDITOR"
2689 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2690 context
.area
.spaces
[0].image
.save_render(fp
)
2691 context
.area
.type = "NODE_EDITOR"
2692 context
.scene
.render
.image_settings
.file_format
= old_render_format
2696 class NWResetNodes(bpy
.types
.Operator
):
2697 """Reset Nodes in Selection"""
2698 bl_idname
= "node.nw_reset_nodes"
2699 bl_label
= "Reset Nodes"
2700 bl_options
= {'REGISTER', 'UNDO'}
2703 def poll(cls
, context
):
2704 return (nw_check(cls
, context
)
2705 and nw_check_selected(cls
, context
)
2706 and nw_check_active(cls
, context
))
2708 def execute(self
, context
):
2709 node_active
= context
.active_node
2710 node_selected
= context
.selected_nodes
2711 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2713 active_node_name
= node_active
.name
if node_active
.select
else None
2714 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2716 # Create output lists
2717 selected_node_names
= [n
.name
for n
in node_selected
]
2720 # Reset all valid children in a frame
2721 node_active_is_frame
= False
2722 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2723 node_tree
= node_active
.id_data
2724 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2726 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2727 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2728 node_active_is_frame
= True
2730 # Check if valid nodes in selection
2731 if not (len(valid_nodes
) > 0):
2732 # Check for frames only
2733 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2734 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2735 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2737 self
.report({'ERROR'}, "No valid node(s) in selection")
2738 return {'CANCELLED'}
2740 # Report nodes that are not valid
2741 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2742 valid_node_names
= [n
.name
for n
in valid_nodes
]
2743 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2744 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2746 # Deselect all nodes
2747 for i
in node_selected
:
2750 # Run through all valid nodes
2751 for node
in valid_nodes
:
2753 parent
= node
.parent
if node
.parent
else None
2754 node_loc
= [node
.location
.x
, node
.location
.y
]
2756 node_tree
= node
.id_data
2757 props_to_copy
= 'bl_idname name location height width'.split(' ')
2760 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2761 for i
in (i
for i
in mappings
if i
.is_linked
):
2763 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2765 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2767 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2768 props_to_copy
.pop(0)
2770 for prop
in props_to_copy
:
2771 setattr(new_node
, prop
, props
[prop
])
2773 nodes
= node_tree
.nodes
2775 new_node
.name
= props
['name']
2778 new_node
.parent
= parent
2779 new_node
.location
= node_loc
2781 for str_from
, str_to
in reconnections
:
2782 connect_sockets(eval(str_from
), eval(str_to
))
2784 new_node
.select
= False
2785 success_names
.append(new_node
.name
)
2787 # Reselect all nodes
2788 if selected_node_names
and node_active_is_frame
is False:
2789 for i
in selected_node_names
:
2790 node_tree
.nodes
[i
].select
= True
2792 if active_node_name
is not None:
2793 node_tree
.nodes
[active_node_name
].select
= True
2794 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2796 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2818 NWAddPrincipledSetup
,
2820 NWLinkActiveToSelected
,
2822 NWSelectParentChildren
,
2828 NWAddMultipleImages
,
2836 from bpy
.utils
import register_class
2842 from bpy
.utils
import unregister_class
2845 unregister_class(cls
)