1 # SPDX-FileCopyrightText: 2013-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 from bpy
.types
import Operator
8 from bpy
.props
import (
17 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
18 from bpy_extras
.node_utils
import connect_sockets
19 from mathutils
import Vector
23 from itertools
import chain
25 from .interface
import NWConnectionListInputs
, NWConnectionListOutputs
27 from .utils
.constants
import blend_types
, geo_combine_operations
, operations
, navs
, get_texture_node_types
, rl_outputs
28 from .utils
.draw
import draw_callback_nodeoutline
29 from .utils
.paths
import match_files_to_socket_names
, split_into_components
30 from .utils
.nodes
import (node_mid_pt
, autolink
, node_at_pos
, get_active_tree
, get_nodes_links
, is_viewer_socket
,
31 is_viewer_link
, get_group_output_node
, get_output_location
, force_update
, get_internal_socket
,
32 nw_check
, NWBase
, get_first_enabled_output
, is_visible_socket
, viewer_socket_name
)
35 class NWLazyMix(Operator
, NWBase
):
36 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
37 bl_idname
= "node.nw_lazy_mix"
38 bl_label
= "Mix Nodes"
39 bl_options
= {'REGISTER', 'UNDO'}
41 def modal(self
, context
, event
):
42 context
.area
.tag_redraw()
43 nodes
, links
= get_nodes_links(context
)
46 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
49 if not context
.scene
.NWBusyDrawing
:
50 node1
= node_at_pos(nodes
, context
, event
)
52 context
.scene
.NWBusyDrawing
= node1
.name
54 if context
.scene
.NWBusyDrawing
!= 'STOP':
55 node1
= nodes
[context
.scene
.NWBusyDrawing
]
57 context
.scene
.NWLazySource
= node1
.name
58 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
60 if event
.type == 'MOUSEMOVE':
61 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
63 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
64 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
65 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
68 node2
= node_at_pos(nodes
, context
, event
)
70 context
.scene
.NWBusyDrawing
= node2
.name
82 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
84 context
.scene
.NWBusyDrawing
= ""
87 elif event
.type == 'ESC':
89 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
92 return {'RUNNING_MODAL'}
94 def invoke(self
, context
, event
):
95 if context
.area
.type == 'NODE_EDITOR':
96 # the arguments we pass the the callback
97 args
= (self
, context
, 'MIX')
98 # Add the region OpenGL drawing callback
99 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
100 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
101 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
105 context
.window_manager
.modal_handler_add(self
)
106 return {'RUNNING_MODAL'}
108 self
.report({'WARNING'}, "View3D not found, cannot run operator")
112 class NWLazyConnect(Operator
, NWBase
):
113 """Connect two nodes without clicking a specific socket (automatically determined"""
114 bl_idname
= "node.nw_lazy_connect"
115 bl_label
= "Lazy Connect"
116 bl_options
= {'REGISTER', 'UNDO'}
117 with_menu
: BoolProperty()
119 def modal(self
, context
, event
):
120 context
.area
.tag_redraw()
121 nodes
, links
= get_nodes_links(context
)
124 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
127 if not context
.scene
.NWBusyDrawing
:
128 node1
= node_at_pos(nodes
, context
, event
)
130 context
.scene
.NWBusyDrawing
= node1
.name
132 if context
.scene
.NWBusyDrawing
!= 'STOP':
133 node1
= nodes
[context
.scene
.NWBusyDrawing
]
135 context
.scene
.NWLazySource
= node1
.name
136 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
138 if event
.type == 'MOUSEMOVE':
139 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
141 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
142 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
143 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
146 node2
= node_at_pos(nodes
, context
, event
)
148 context
.scene
.NWBusyDrawing
= node2
.name
161 original_sel
.append(node
)
163 original_unsel
.append(node
)
167 # link_success = autolink(node1, node2, links)
169 if len(node1
.outputs
) > 1 and node2
.inputs
:
170 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
171 elif len(node1
.outputs
) == 1:
172 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
174 link_success
= autolink(node1
, node2
, links
)
176 for node
in original_sel
:
178 for node
in original_unsel
:
182 force_update(context
)
183 context
.scene
.NWBusyDrawing
= ""
186 elif event
.type == 'ESC':
187 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
190 return {'RUNNING_MODAL'}
192 def invoke(self
, context
, event
):
193 if context
.area
.type == 'NODE_EDITOR':
194 nodes
, links
= get_nodes_links(context
)
195 node
= node_at_pos(nodes
, context
, event
)
197 context
.scene
.NWBusyDrawing
= node
.name
199 # the arguments we pass the the callback
203 args
= (self
, context
, mode
)
204 # Add the region OpenGL drawing callback
205 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
206 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
207 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
211 context
.window_manager
.modal_handler_add(self
)
212 return {'RUNNING_MODAL'}
214 self
.report({'WARNING'}, "View3D not found, cannot run operator")
218 class NWDeleteUnused(Operator
, NWBase
):
219 """Delete all nodes whose output is not used"""
220 bl_idname
= 'node.nw_del_unused'
221 bl_label
= 'Delete Unused Nodes'
222 bl_options
= {'REGISTER', 'UNDO'}
224 delete_muted
: BoolProperty(
226 description
="Delete (but reconnect, like Ctrl-X) all muted nodes",
228 delete_frames
: BoolProperty(
229 name
="Delete Empty Frames",
230 description
="Delete all frames that have no nodes inside them",
233 def is_unused_node(self
, node
):
234 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
235 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
236 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
237 if node
.type in end_types
:
240 for output
in node
.outputs
:
246 def poll(cls
, context
):
248 if nw_check(context
):
249 if context
.space_data
.node_tree
.nodes
:
253 def execute(self
, context
):
254 nodes
, links
= get_nodes_links(context
)
260 selection
.append(node
.name
)
266 temp_deleted_nodes
= []
267 del_unused_iterations
= len(nodes
)
268 for it
in range(0, del_unused_iterations
):
269 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
271 if self
.is_unused_node(node
):
273 deleted_nodes
.append(node
.name
)
274 bpy
.ops
.node
.delete()
276 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
279 if self
.delete_frames
:
287 frames_in_use
.append(node
.parent
)
289 if node
.type == 'FRAME' and node
not in frames_in_use
:
292 repeat
= True # repeat for nested frames
294 if node
not in frames_in_use
:
296 deleted_nodes
.append(node
.name
)
297 bpy
.ops
.node
.delete()
299 if self
.delete_muted
:
303 deleted_nodes
.append(node
.name
)
304 bpy
.ops
.node
.delete_reconnect()
306 # get unique list of deleted nodes (iterations would count the same node more than once)
307 deleted_nodes
= list(set(deleted_nodes
))
308 for n
in deleted_nodes
:
309 self
.report({'INFO'}, "Node " + n
+ " deleted")
310 num_deleted
= len(deleted_nodes
)
315 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
317 self
.report({'INFO'}, "Nothing deleted")
320 nodes
, links
= get_nodes_links(context
)
322 if node
.name
in selection
:
326 def invoke(self
, context
, event
):
327 return context
.window_manager
.invoke_confirm(self
, event
)
330 class NWSwapLinks(Operator
, NWBase
):
331 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
332 bl_idname
= 'node.nw_swap_links'
333 bl_label
= 'Swap Links'
334 bl_options
= {'REGISTER', 'UNDO'}
337 def poll(cls
, context
):
339 if nw_check(context
):
340 if context
.selected_nodes
:
341 valid
= len(context
.selected_nodes
) <= 2
344 def execute(self
, context
):
345 nodes
, links
= get_nodes_links(context
)
346 selected_nodes
= context
.selected_nodes
347 n1
= selected_nodes
[0]
350 if len(selected_nodes
) == 2:
351 n2
= selected_nodes
[1]
352 if n1
.outputs
and n2
.outputs
:
357 for output
in n1
.outputs
:
359 for link
in output
.links
:
360 n1_outputs
.append([out_index
, link
.to_socket
])
365 for output
in n2
.outputs
:
367 for link
in output
.links
:
368 n2_outputs
.append([out_index
, link
.to_socket
])
372 for connection
in n1_outputs
:
374 connect_sockets(n2
.outputs
[connection
[0]], connection
[1])
376 self
.report({'WARNING'},
377 "Some connections have been lost due to differing numbers of output sockets")
378 for connection
in n2_outputs
:
380 connect_sockets(n1
.outputs
[connection
[0]], connection
[1])
382 self
.report({'WARNING'},
383 "Some connections have been lost due to differing numbers of output sockets")
385 if n1
.outputs
or n2
.outputs
:
386 self
.report({'WARNING'}, "One of the nodes has no outputs!")
388 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
391 elif len(selected_nodes
) == 1:
392 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
393 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
399 if i1
.is_linked
and not i1
.is_multi_input
:
402 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
404 types
.append([i1
, similar_types
, i
])
406 types
.sort(key
=lambda k
: k
[1], reverse
=True)
412 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
414 i1f
= pair
[0].links
[0].from_socket
415 i1t
= pair
[0].links
[0].to_socket
416 i2f
= pair
[1].links
[0].from_socket
417 i2t
= pair
[1].links
[0].to_socket
418 connect_sockets(i1f
, i2t
)
419 connect_sockets(i2f
, i1t
)
422 fs
= t
[0].links
[0].from_socket
424 links
.remove(t
[0].links
[0])
425 if i
+ 1 == len(n1
.inputs
):
428 while n1
.inputs
[i
].is_linked
:
430 connect_sockets(fs
, n1
.inputs
[i
])
431 elif len(types
) == 2:
432 i1f
= types
[0][0].links
[0].from_socket
433 i1t
= types
[0][0].links
[0].to_socket
434 i2f
= types
[1][0].links
[0].from_socket
435 i2t
= types
[1][0].links
[0].to_socket
436 connect_sockets(i1f
, i2t
)
437 connect_sockets(i2f
, i1t
)
440 self
.report({'WARNING'}, "This node has no input connections to swap!")
442 self
.report({'WARNING'}, "This node has no inputs to swap!")
444 force_update(context
)
448 class NWResetBG(Operator
, NWBase
):
449 """Reset the zoom and position of the background image"""
450 bl_idname
= 'node.nw_bg_reset'
451 bl_label
= 'Reset Backdrop'
452 bl_options
= {'REGISTER', 'UNDO'}
455 def poll(cls
, context
):
457 if nw_check(context
):
458 snode
= context
.space_data
459 valid
= snode
.tree_type
== 'CompositorNodeTree'
462 def execute(self
, context
):
463 context
.space_data
.backdrop_zoom
= 1
464 context
.space_data
.backdrop_offset
[0] = 0
465 context
.space_data
.backdrop_offset
[1] = 0
469 class NWAddAttrNode(Operator
, NWBase
):
470 """Add an Attribute node with this name"""
471 bl_idname
= 'node.nw_add_attr_node'
472 bl_label
= 'Add UV map'
473 bl_options
= {'REGISTER', 'UNDO'}
475 attr_name
: StringProperty()
477 def execute(self
, context
):
478 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
479 nodes
, links
= get_nodes_links(context
)
480 nodes
.active
.attribute_name
= self
.attr_name
484 class NWPreviewNode(Operator
, NWBase
):
485 bl_idname
= "node.nw_preview_node"
486 bl_label
= "Preview Node"
487 bl_description
= "Connect active node to the Node Group output or the Material Output"
488 bl_options
= {'REGISTER', 'UNDO'}
490 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
491 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
492 run_in_geometry_nodes
: BoolProperty(default
=True)
495 self
.shader_output_type
= ""
496 self
.shader_output_ident
= ""
499 def poll(cls
, context
):
500 if nw_check(context
):
501 space
= context
.space_data
502 if space
.tree_type
in {'ShaderNodeTree', 'GeometryNodeTree'}:
503 if context
.active_node
:
504 if context
.active_node
.type not in {"OUTPUT_MATERIAL", "OUTPUT_WORLD"}:
511 def get_output_sockets(node_tree
):
512 return [item
for item
in node_tree
.interface
.items_tree
if item
.item_type
== 'SOCKET' and item
.in_out
in {'OUTPUT', 'BOTH'}]
514 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
515 """Check if a viewer output already exists in a node group, otherwise create it"""
516 if not hasattr(node
, "node_tree"):
520 output_sockets
= self
.get_output_sockets(node
.node_tree
)
521 if len(output_sockets
):
522 for i
, socket
in enumerate(output_sockets
):
523 if is_viewer_socket(socket
) and socket
.socket_type
== socket_type
:
524 # If viewer output is already used but leads to the same socket we can still use it
525 is_used
= self
.is_socket_used_other_mats(socket
)
527 if connect_socket
is None:
529 groupout
= get_group_output_node(node
.node_tree
)
530 groupout_input
= groupout
.inputs
[i
]
531 links
= groupout_input
.links
532 if connect_socket
not in [link
.from_socket
for link
in links
]:
534 viewer_socket
= socket
537 if viewer_socket
is None:
538 # Create viewer socket
539 viewer_socket
= node
.node_tree
.interface
.new_socket(viewer_socket_name
, in_out
='OUTPUT', socket_type
=socket_type
)
540 viewer_socket
.NWViewerSocket
= True
543 def init_shader_variables(self
, space
, shader_type
):
544 if shader_type
== 'OBJECT':
545 if space
.id in bpy
.data
.lights
.values():
546 self
.shader_output_type
= "OUTPUT_LIGHT"
547 self
.shader_output_ident
= "ShaderNodeOutputLight"
549 self
.shader_output_type
= "OUTPUT_MATERIAL"
550 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
552 elif shader_type
== 'WORLD':
553 self
.shader_output_type
= "OUTPUT_WORLD"
554 self
.shader_output_ident
= "ShaderNodeOutputWorld"
557 def ensure_group_output(tree
):
558 """Check if a group output node exists, otherwise create it"""
559 groupout
= get_group_output_node(tree
)
561 groupout
= tree
.nodes
.new('NodeGroupOutput')
562 loc_x
, loc_y
= get_output_location(tree
)
563 groupout
.location
.x
= loc_x
564 groupout
.location
.y
= loc_y
565 groupout
.select
= False
566 # So that we don't keep on adding new group outputs
567 groupout
.is_active_output
= True
571 def search_sockets(cls
, node
, sockets
, index
=None):
572 """Recursively scan nodes for viewer sockets and store them in a list"""
573 for i
, input_socket
in enumerate(node
.inputs
):
574 if index
and i
!= index
:
576 if len(input_socket
.links
):
577 link
= input_socket
.links
[0]
578 next_node
= link
.from_node
579 external_socket
= link
.from_socket
580 if hasattr(next_node
, "node_tree"):
581 for socket_index
, socket
in enumerate(next_node
.node_tree
.interface
.items_tree
):
582 if socket
.identifier
== external_socket
.identifier
:
584 if is_viewer_socket(socket
) and socket
not in sockets
:
585 sockets
.append(socket
)
586 # continue search inside of node group but restrict socket to where we came from
587 groupout
= get_group_output_node(next_node
.node_tree
)
588 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
591 def scan_nodes(cls
, tree
, sockets
):
592 """Recursively get all viewer sockets in a material tree"""
593 for node
in tree
.nodes
:
594 if hasattr(node
, "node_tree"):
595 if node
.node_tree
is None:
597 for socket
in cls
.get_output_sockets(node
.node_tree
):
598 if is_viewer_socket(socket
) and (socket
not in sockets
):
599 sockets
.append(socket
)
600 cls
.scan_nodes(node
.node_tree
, sockets
)
603 def remove_socket(tree
, socket
):
604 interface
= tree
.interface
605 interface
.remove(socket
)
606 interface
.active_index
= min(interface
.active_index
, len(interface
.items_tree
) - 1)
608 def link_leads_to_used_socket(self
, link
):
609 """Return True if link leads to a socket that is already used in this material"""
610 socket
= get_internal_socket(link
.to_socket
)
611 return (socket
and self
.is_socket_used_active_mat(socket
))
613 def is_socket_used_active_mat(self
, socket
):
614 """Ensure used sockets in active material is calculated and check given socket"""
615 if not hasattr(self
, "used_viewer_sockets_active_mat"):
616 self
.used_viewer_sockets_active_mat
= []
617 output_node
= get_group_output_node(bpy
.context
.space_data
.node_tree
,
618 output_node_type
=self
.shader_output_type
)
620 if output_node
is not None:
621 self
.search_sockets(output_node
, self
.used_viewer_sockets_active_mat
)
622 return socket
in self
.used_viewer_sockets_active_mat
624 def is_socket_used_other_mats(self
, socket
):
625 """Ensure used sockets in other materials are calculated and check given socket"""
626 if not hasattr(self
, "used_viewer_sockets_other_mats"):
627 self
.used_viewer_sockets_other_mats
= []
628 for mat
in bpy
.data
.materials
:
629 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
632 output_node
= get_group_output_node(mat
.node_tree
,
633 output_node_type
=self
.shader_output_type
)
634 if output_node
is not None:
635 self
.search_sockets(output_node
, self
.used_viewer_sockets_other_mats
)
636 return socket
in self
.used_viewer_sockets_other_mats
638 def get_output_index(self
, base_node_tree
, nodes
, output_node
, socket_type
, check_type
=False):
639 """Get the next available output socket in the active node"""
642 for i
, out
in enumerate(nodes
.active
.outputs
):
643 if is_visible_socket(out
) and (not check_type
or out
.type == socket_type
):
644 valid_outputs
.append(i
)
646 out_i
= valid_outputs
[0] # Start index of node's outputs
647 for i
, valid_i
in enumerate(valid_outputs
):
648 for out_link
in nodes
.active
.outputs
[valid_i
].links
:
649 if is_viewer_link(out_link
, output_node
):
650 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
651 if i
< len(valid_outputs
) - 1:
652 out_i
= valid_outputs
[i
+ 1]
654 out_i
= valid_outputs
[0]
657 def create_links(self
, tree
, link_end
, active
, out_i
, socket_type
):
658 """Create links through node groups until we reach the active node"""
659 while tree
.nodes
.active
!= active
:
660 node
= tree
.nodes
.active
661 viewer_socket
= self
.ensure_viewer_socket(
662 node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
663 link_start
= node
.outputs
[viewer_socket
.identifier
]
664 if viewer_socket
in self
.delete_sockets
:
665 self
.delete_sockets
.remove(viewer_socket
)
666 connect_sockets(link_start
, link_end
)
668 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[viewer_socket
.identifier
]
669 tree
= tree
.nodes
.active
.node_tree
670 connect_sockets(active
.outputs
[out_i
], link_end
)
672 def invoke(self
, context
, event
):
673 space
= context
.space_data
674 # Ignore operator when running in wrong context.
675 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
676 return {'PASS_THROUGH'}
678 mlocx
= event
.mouse_region_x
679 mlocy
= event
.mouse_region_y
680 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
681 if not 'FINISHED' in select_node
: # only run if mouse click is on a node
684 active_tree
, path_to_tree
= get_active_tree(context
)
685 nodes
, links
= active_tree
.nodes
, active_tree
.links
686 base_node_tree
= space
.node_tree
687 active
= nodes
.active
689 if not active
and not any(is_visible_socket(out
) for out
in active
.outputs
):
692 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
693 self
.delete_sockets
= []
694 self
.scan_nodes(base_node_tree
, self
.delete_sockets
)
696 # For geometry node trees we just connect to the group output
697 if space
.tree_type
== "GeometryNodeTree" and active
.outputs
:
698 socket_type
= 'GEOMETRY'
700 # Find (or create if needed) the output of this node tree
701 output_node
= self
.ensure_group_output(base_node_tree
)
703 out_i
= self
.get_output_index(base_node_tree
, nodes
, output_node
, 'GEOMETRY', check_type
=True)
704 # If there is no 'GEOMETRY' output type - We can't preview the node
708 # Find an input socket of the output of type geometry
709 geometry_out_index
= None
710 for i
, inp
in enumerate(output_node
.inputs
):
711 if inp
.type == socket_type
:
712 geometry_out_index
= i
714 if geometry_out_index
is None:
715 # Create geometry socket
716 geometry_out_socket
= base_node_tree
.interface
.new_socket(
717 'Geometry', in_out
='OUTPUT', socket_type
='NodeSocketGeometry'
719 geometry_out_index
= geometry_out_socket
.index
721 output_socket
= output_node
.inputs
[geometry_out_index
]
723 self
.create_links(base_node_tree
, output_socket
, active
, out_i
, 'NodeSocketGeometry')
725 # What follows is code for the shader editor
726 elif space
.tree_type
== "ShaderNodeTree" and active
.outputs
:
727 shader_type
= space
.shader_type
728 self
.init_shader_variables(space
, shader_type
)
729 socket_type
= 'NodeSocketShader'
731 # Get or create material_output node
732 output_node
= get_group_output_node(base_node_tree
,
733 output_node_type
=self
.shader_output_type
)
735 output_node
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
736 output_node
.location
= get_output_location(base_node_tree
)
737 output_node
.select
= False
739 out_i
= self
.get_output_index(base_node_tree
, nodes
, output_node
, 'SHADER')
741 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
742 output_socket
= output_node
.inputs
[materialout_index
]
744 self
.create_links(base_node_tree
, output_socket
, active
, out_i
, 'NodeSocketShader')
747 for socket
in self
.delete_sockets
:
748 if not self
.is_socket_used_other_mats(socket
):
749 tree
= socket
.id_data
750 self
.remove_socket(tree
, socket
)
752 nodes
.active
= active
754 force_update(context
)
758 class NWFrameSelected(Operator
, NWBase
):
759 bl_idname
= "node.nw_frame_selected"
760 bl_label
= "Frame Selected"
761 bl_description
= "Add a frame node and parent the selected nodes to it"
762 bl_options
= {'REGISTER', 'UNDO'}
764 label_prop
: StringProperty(
766 description
='The visual name of the frame node',
769 use_custom_color_prop
: BoolProperty(
771 description
="Use custom color for the frame node",
774 color_prop
: FloatVectorProperty(
776 description
="The color of the frame node",
777 default
=(0.604, 0.604, 0.604),
778 min=0, max=1, step
=1, precision
=3,
779 subtype
='COLOR_GAMMA', size
=3
782 def draw(self
, context
):
784 layout
.prop(self
, 'label_prop')
785 layout
.prop(self
, 'use_custom_color_prop')
786 col
= layout
.column()
787 col
.active
= self
.use_custom_color_prop
788 col
.prop(self
, 'color_prop', text
="")
790 def execute(self
, context
):
791 nodes
, links
= get_nodes_links(context
)
795 selected
.append(node
)
797 bpy
.ops
.node
.add_node(type='NodeFrame')
799 frm
.label
= self
.label_prop
800 frm
.use_custom_color
= self
.use_custom_color_prop
801 frm
.color
= self
.color_prop
803 for node
in selected
:
809 class NWReloadImages(Operator
):
810 bl_idname
= "node.nw_reload_images"
811 bl_label
= "Reload Images"
812 bl_description
= "Update all the image nodes to match their files on disk"
815 def poll(cls
, context
):
817 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
818 if context
.active_node
is not None:
819 for out
in context
.active_node
.outputs
:
820 if is_visible_socket(out
):
825 def execute(self
, context
):
826 nodes
, links
= get_nodes_links(context
)
827 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
830 if node
.type in image_types
:
831 if node
.type == "TEXTURE":
832 if node
.texture
: # node has texture assigned
833 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
834 if node
.texture
.image
: # texture has image assigned
835 node
.texture
.image
.reload()
843 self
.report({'INFO'}, "Reloaded images")
844 print("Reloaded " + str(num_reloaded
) + " images")
845 force_update(context
)
848 self
.report({'WARNING'}, "No images found to reload in this node tree")
852 class NWMergeNodes(Operator
, NWBase
):
853 bl_idname
= "node.nw_merge_nodes"
854 bl_label
= "Merge Nodes"
855 bl_description
= "Merge Selected Nodes"
856 bl_options
= {'REGISTER', 'UNDO'}
860 description
="All possible blend types, boolean operations and math operations",
861 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
],
863 merge_type
: EnumProperty(
865 description
="Type of Merge to be used",
867 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
868 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
869 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
870 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
871 ('MATH', 'Math Node', 'Merge using Math Nodes'),
872 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
873 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
877 # Check if the link connects to a node that is in selected_nodes
878 # If not, then check recursively for each link in the nodes outputs.
879 # If yes, return True. If the recursion stops without finding a node
880 # in selected_nodes, it returns False. The depth is used to prevent
881 # getting stuck in a loop because of an already present cycle.
883 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
885 # We're stuck in a cycle, but that cycle was already present,
886 # so we return False.
887 # NOTE: The number 255 is arbitrary, but seems to work well.
890 if node
in selected_nodes
:
894 for output
in node
.outputs
:
896 for olink
in output
.links
:
897 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
899 # None of the outputs found a node in selected_nodes, so there is no cycle.
902 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
903 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
904 # be connected. The last one is assumed to be a multi input socket.
905 # For convenience the node is returned.
907 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
908 # The y-location of the last node
909 loc_y
= nodes_list
[-1][2]
910 if merge_position
== 'CENTER':
911 # Average the y-location
912 for i
in range(len(nodes_list
) - 1):
913 loc_y
+= nodes_list
[i
][2]
914 loc_y
= loc_y
/ len(nodes_list
)
915 new_node
= nodes
.new(node_name
)
916 new_node
.hide
= do_hide
917 new_node
.location
.x
= loc_x
918 new_node
.location
.y
= loc_y
919 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
921 outputs_for_multi_input
= []
922 for i
, node
in enumerate(selected_nodes
):
924 # Search for the first node which had output links that do not create
925 # a cycle, which we can then reconnect afterwards.
926 if prev_links
== [] and node
.outputs
[0].is_linked
:
928 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
929 link
, selected_nodes
)]
930 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
931 # To get the placement to look right we need to reverse the order in which we connect the
932 # outputs to the multi input socket.
933 if i
< len(socket_indices
) - 1:
934 ind
= socket_indices
[i
]
935 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
937 outputs_for_multi_input
.insert(0, node
.outputs
[0])
938 if outputs_for_multi_input
!= []:
939 ind
= socket_indices
[-1]
940 for output
in outputs_for_multi_input
:
941 connect_sockets(output
, new_node
.inputs
[ind
])
943 for link
in prev_links
:
944 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
947 def execute(self
, context
):
948 settings
= context
.preferences
.addons
[__package__
].preferences
949 merge_hide
= settings
.merge_hide
950 merge_position
= settings
.merge_position
# 'center' or 'bottom'
953 do_hide_shader
= False
954 if merge_hide
== 'ALWAYS':
956 do_hide_shader
= True
957 elif merge_hide
== 'NON_SHADER':
960 tree_type
= context
.space_data
.node_tree
.type
961 if tree_type
== 'GEOMETRY':
962 node_type
= 'GeometryNode'
963 if tree_type
== 'COMPOSITING':
964 node_type
= 'CompositorNode'
965 elif tree_type
== 'SHADER':
966 node_type
= 'ShaderNode'
967 elif tree_type
== 'TEXTURE':
968 node_type
= 'TextureNode'
969 nodes
, links
= get_nodes_links(context
)
971 merge_type
= self
.merge_type
972 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
973 # 'ZCOMBINE' works only if mode == 'MIX'
974 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
975 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
978 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
980 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
981 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
982 node_type
= 'ShaderNode'
983 selected_mix
= [] # entry = [index, loc]
984 selected_shader
= [] # entry = [index, loc]
985 selected_geometry
= [] # entry = [index, loc]
986 selected_math
= [] # entry = [index, loc]
987 selected_vector
= [] # entry = [index, loc]
988 selected_z
= [] # entry = [index, loc]
989 selected_alphaover
= [] # entry = [index, loc]
991 for i
, node
in enumerate(nodes
):
992 if node
.select
and node
.outputs
:
993 if merge_type
== 'AUTO':
994 for (type, types_list
, dst
) in (
995 ('SHADER', ('MIX', 'ADD'), selected_shader
),
996 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
997 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
998 ('VALUE', [t
[0] for t
in operations
], selected_math
),
999 ('VECTOR', [], selected_vector
),
1001 output
= get_first_enabled_output(node
)
1002 output_type
= output
.type
1003 valid_mode
= mode
in types_list
1004 # When mode is 'MIX' we have to cheat since the mix node is not used in
1006 if tree_type
== 'GEOMETRY':
1008 if output_type
== 'VALUE' and type == 'VALUE':
1010 elif output_type
== 'VECTOR' and type == 'VECTOR':
1012 elif type == 'GEOMETRY':
1014 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1015 # Cheat that output type is 'RGBA',
1016 # and that 'MIX' exists in math operations list.
1017 # This way when selected_mix list is analyzed:
1018 # Node data will be appended even though it doesn't meet requirements.
1019 elif output_type
!= 'SHADER' and mode
== 'MIX':
1020 output_type
= 'RGBA'
1022 if output_type
== type and valid_mode
:
1023 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1025 for (type, types_list
, dst
) in (
1026 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1027 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1028 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1029 ('MATH', [t
[0] for t
in operations
], selected_math
),
1030 ('ZCOMBINE', ('MIX', ), selected_z
),
1031 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1033 if merge_type
== type and mode
in types_list
:
1034 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1035 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1036 # use only 'Mix' nodes for merging.
1037 # For that we add selected_math list to selected_mix list and clear selected_math.
1038 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1039 selected_mix
+= selected_math
1048 selected_alphaover
]:
1051 count_before
= len(nodes
)
1052 # sort list by loc_x - reversed
1053 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1055 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1056 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1058 # Change the node type for math nodes in a geometry node tree.
1059 if tree_type
== 'GEOMETRY':
1060 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1061 node_type
= 'ShaderNode'
1065 node_type
= 'GeometryNode'
1066 if merge_position
== 'CENTER':
1067 # average yloc of last two nodes (lowest two)
1068 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1069 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1075 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1079 if nodes_list
== selected_shader
and not do_hide_shader
:
1081 the_range
= len(nodes_list
) - 1
1082 if len(nodes_list
) == 1:
1085 for i
in range(the_range
):
1086 if nodes_list
== selected_mix
:
1088 if tree_type
== 'COMPOSITING':
1090 add_type
= node_type
+ mix_name
1091 add
= nodes
.new(add_type
)
1092 if tree_type
!= 'COMPOSITING':
1093 add
.data_type
= 'RGBA'
1094 add
.blend_type
= mode
1096 add
.inputs
[0].default_value
= 1.0
1097 add
.show_preview
= False
1103 if tree_type
== 'COMPOSITING':
1106 elif nodes_list
== selected_math
:
1107 add_type
= node_type
+ 'Math'
1108 add
= nodes
.new(add_type
)
1109 add
.operation
= mode
1115 elif nodes_list
== selected_shader
:
1117 add_type
= node_type
+ 'MixShader'
1118 add
= nodes
.new(add_type
)
1119 add
.hide
= do_hide_shader
1125 add_type
= node_type
+ 'AddShader'
1126 add
= nodes
.new(add_type
)
1127 add
.hide
= do_hide_shader
1132 elif nodes_list
== selected_geometry
:
1133 if mode
in ('JOIN', 'MIX'):
1134 add_type
= node_type
+ 'JoinGeometry'
1135 add
= self
.merge_with_multi_input(
1136 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1138 add_type
= node_type
+ 'MeshBoolean'
1139 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1140 add
= self
.merge_with_multi_input(
1141 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1142 add
.operation
= mode
1145 elif nodes_list
== selected_vector
:
1146 add_type
= node_type
+ 'VectorMath'
1147 add
= nodes
.new(add_type
)
1148 add
.operation
= mode
1154 elif nodes_list
== selected_z
:
1155 add
= nodes
.new('CompositorNodeZcombine')
1156 add
.show_preview
= False
1162 elif nodes_list
== selected_alphaover
:
1163 add
= nodes
.new('CompositorNodeAlphaOver')
1164 add
.show_preview
= False
1170 add
.location
= loc_x
, loc_y
1174 # This has already been handled separately
1178 count_after
= len(nodes
)
1179 index
= count_after
- 1
1180 first_selected
= nodes
[nodes_list
[0][0]]
1181 # "last" node has been added as first, so its index is count_before.
1182 last_add
= nodes
[count_before
]
1183 # Create list of invalid indexes.
1184 invalid_nodes
= [nodes
[n
[0]]
1185 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1188 # Two nodes were selected and first selected has no output links, second selected has output links.
1189 # Then add links from last add to all links 'to_socket' of out links of second selected.
1190 first_selected_output
= get_first_enabled_output(first_selected
)
1191 if len(nodes_list
) == 2:
1192 if not first_selected_output
.links
:
1193 second_selected
= nodes
[nodes_list
[1][0]]
1194 for ss_link
in get_first_enabled_output(second_selected
).links
:
1195 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1196 # Link only if "to_node" index not in invalid indexes list.
1197 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1198 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1199 # add links from last_add to all links 'to_socket' of out links of first selected.
1200 for fs_link
in first_selected_output
.links
:
1201 # Link only if "to_node" index not in invalid indexes list.
1202 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1203 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1204 # add link from "first" selected and "first" add node
1205 node_to
= nodes
[count_after
- 1]
1206 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1207 if node_to
.type == 'ZCOMBINE':
1208 for fs_out
in first_selected
.outputs
:
1209 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1210 connect_sockets(fs_out
, node_to
.inputs
[1])
1212 # add links between added ADD nodes and between selected and ADD nodes
1213 for i
in range(count_adds
):
1214 if i
< count_adds
- 1:
1215 node_from
= nodes
[index
]
1216 node_to
= nodes
[index
- 1]
1217 node_to_input_i
= first
1218 node_to_z_i
= 1 # if z combine - link z to first z input
1219 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1220 if node_to
.type == 'ZCOMBINE':
1221 for from_out
in node_from
.outputs
:
1222 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1223 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1224 if len(nodes_list
) > 1:
1225 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1226 node_to
= nodes
[index
]
1227 node_to_input_i
= second
1228 node_to_z_i
= 3 # if z combine - link z to second z input
1229 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1230 if node_to
.type == 'ZCOMBINE':
1231 for from_out
in node_from
.outputs
:
1232 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1233 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1235 # set "last" of added nodes as active
1236 nodes
.active
= last_add
1237 for i
, x
, y
, dx
, h
in nodes_list
:
1238 nodes
[i
].select
= False
1243 class NWBatchChangeNodes(Operator
, NWBase
):
1244 bl_idname
= "node.nw_batch_change"
1245 bl_label
= "Batch Change"
1246 bl_description
= "Batch Change Blend Type and Math Operation"
1247 bl_options
= {'REGISTER', 'UNDO'}
1249 blend_type
: EnumProperty(
1251 items
=blend_types
+ navs
,
1253 operation
: EnumProperty(
1255 items
=operations
+ navs
,
1258 def execute(self
, context
):
1259 blend_type
= self
.blend_type
1260 operation
= self
.operation
1261 for node
in context
.selected_nodes
:
1262 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1263 if blend_type
not in [nav
[0] for nav
in navs
]:
1264 node
.blend_type
= blend_type
1266 if blend_type
== 'NEXT':
1267 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1268 # index = blend_types.index(node.blend_type)
1269 if index
== len(blend_types
) - 1:
1270 node
.blend_type
= blend_types
[0][0]
1272 node
.blend_type
= blend_types
[index
+ 1][0]
1274 if blend_type
== 'PREV':
1275 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1277 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1279 node
.blend_type
= blend_types
[index
- 1][0]
1281 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1282 if operation
not in [nav
[0] for nav
in navs
]:
1283 node
.operation
= operation
1285 if operation
== 'NEXT':
1286 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1287 # index = operations.index(node.operation)
1288 if index
== len(operations
) - 1:
1289 node
.operation
= operations
[0][0]
1291 node
.operation
= operations
[index
+ 1][0]
1293 if operation
== 'PREV':
1294 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1295 # index = operations.index(node.operation)
1297 node
.operation
= operations
[len(operations
) - 1][0]
1299 node
.operation
= operations
[index
- 1][0]
1304 class NWChangeMixFactor(Operator
, NWBase
):
1305 bl_idname
= "node.nw_factor"
1306 bl_label
= "Change Factor"
1307 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1308 bl_options
= {'REGISTER', 'UNDO'}
1310 # option: Change factor.
1311 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1312 # Else - change factor by option value.
1313 option
: FloatProperty()
1315 def execute(self
, context
):
1316 nodes
, links
= get_nodes_links(context
)
1317 option
= self
.option
1318 selected
= [] # entry = index
1319 for si
, node
in enumerate(nodes
):
1321 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1325 fac
= nodes
[si
].inputs
[0]
1326 nodes
[si
].hide
= False
1327 if option
in {0.0, 1.0}:
1328 fac
.default_value
= option
1330 fac
.default_value
+= option
1335 class NWCopySettings(Operator
, NWBase
):
1336 bl_idname
= "node.nw_copy_settings"
1337 bl_label
= "Copy Settings"
1338 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1339 bl_options
= {'REGISTER', 'UNDO'}
1342 def poll(cls
, context
):
1344 if nw_check(context
):
1346 context
.active_node
is not None and
1347 context
.active_node
.type != 'FRAME'
1352 def execute(self
, context
):
1353 node_active
= context
.active_node
1354 node_selected
= context
.selected_nodes
1357 if not (len(node_selected
) > 1):
1358 self
.report({'ERROR'}, "2 nodes must be selected at least")
1359 return {'CANCELLED'}
1361 # Check if active node is in the selection
1362 selected_node_names
= [n
.name
for n
in node_selected
]
1363 if node_active
.name
not in selected_node_names
:
1364 self
.report({'ERROR'}, "No active node")
1365 return {'CANCELLED'}
1367 # Get nodes in selection by type
1368 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1370 if not (len(valid_nodes
) > 1) and node_active
:
1371 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1372 return {'CANCELLED'}
1374 if len(valid_nodes
) != len(node_selected
):
1375 # Report nodes that are not valid
1376 valid_node_names
= [n
.name
for n
in valid_nodes
]
1377 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1380 "Ignored {} (not of the same type as {})".format(
1381 ", ".join(not_valid_names
),
1384 # Reference original
1386 # node_selected_names = [n.name for n in node_selected]
1391 # Deselect all nodes
1392 for i
in node_selected
:
1395 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1396 # Run through all other nodes
1397 for node
in valid_nodes
[1:]:
1399 # Check for frame node
1400 parent
= node
.parent
if node
.parent
else None
1401 node_loc
= [node
.location
.x
, node
.location
.y
]
1403 # Select original to duplicate
1406 # Duplicate selected node
1407 bpy
.ops
.node
.duplicate()
1408 new_node
= context
.selected_nodes
[0]
1411 new_node
.select
= False
1413 # Properties to copy
1414 node_tree
= node
.id_data
1415 props_to_copy
= 'bl_idname name location height width'.split(' ')
1419 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1420 for i
in (i
for i
in mappings
if i
.is_linked
):
1422 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1425 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1426 props_to_copy
.pop(0)
1428 for prop
in props_to_copy
:
1429 setattr(new_node
, prop
, props
[prop
])
1431 # Get the node tree to remove the old node
1432 nodes
= node_tree
.nodes
1434 new_node
.name
= props
['name']
1437 new_node
.parent
= parent
1438 new_node
.location
= node_loc
1440 for str_from
, str_to
in reconnections
:
1441 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1443 success_names
.append(new_node
.name
)
1446 node_tree
.nodes
.active
= orig
1449 "Successfully copied attributes from {} to: {}".format(
1451 ", ".join(success_names
)))
1455 class NWCopyLabel(Operator
, NWBase
):
1456 bl_idname
= "node.nw_copy_label"
1457 bl_label
= "Copy Label"
1458 bl_options
= {'REGISTER', 'UNDO'}
1460 option
: EnumProperty(
1462 description
="Source of name of label",
1464 ('FROM_ACTIVE', 'from active', 'from active node',),
1465 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1466 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1470 def execute(self
, context
):
1471 nodes
, links
= get_nodes_links(context
)
1472 option
= self
.option
1473 active
= nodes
.active
1474 if option
== 'FROM_ACTIVE':
1476 src_label
= active
.label
1477 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1478 node
.label
= src_label
1479 elif option
== 'FROM_NODE':
1480 selected
= [n
for n
in nodes
if n
.select
]
1481 for node
in selected
:
1482 for input in node
.inputs
:
1484 src
= input.links
[0].from_node
1485 node
.label
= src
.label
1487 elif option
== 'FROM_SOCKET':
1488 selected
= [n
for n
in nodes
if n
.select
]
1489 for node
in selected
:
1490 for input in node
.inputs
:
1492 src
= input.links
[0].from_socket
1493 node
.label
= src
.name
1499 class NWClearLabel(Operator
, NWBase
):
1500 bl_idname
= "node.nw_clear_label"
1501 bl_label
= "Clear Label"
1502 bl_options
= {'REGISTER', 'UNDO'}
1504 option
: BoolProperty()
1506 def execute(self
, context
):
1507 nodes
, links
= get_nodes_links(context
)
1508 for node
in [n
for n
in nodes
if n
.select
]:
1513 def invoke(self
, context
, event
):
1515 return self
.execute(context
)
1517 return context
.window_manager
.invoke_confirm(self
, event
)
1520 class NWModifyLabels(Operator
, NWBase
):
1521 """Modify Labels of all selected nodes"""
1522 bl_idname
= "node.nw_modify_labels"
1523 bl_label
= "Modify Labels"
1524 bl_options
= {'REGISTER', 'UNDO'}
1526 prepend
: StringProperty(
1527 name
="Add to Beginning"
1529 append
: StringProperty(
1532 replace_from
: StringProperty(
1533 name
="Text to Replace"
1535 replace_to
: StringProperty(
1539 def execute(self
, context
):
1540 nodes
, links
= get_nodes_links(context
)
1541 for node
in [n
for n
in nodes
if n
.select
]:
1542 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1546 def invoke(self
, context
, event
):
1550 return context
.window_manager
.invoke_props_dialog(self
)
1553 class NWAddTextureSetup(Operator
, NWBase
):
1554 bl_idname
= "node.nw_add_texture"
1555 bl_label
= "Texture Setup"
1556 bl_description
= "Add Texture Node Setup to Selected Shaders"
1557 bl_options
= {'REGISTER', 'UNDO'}
1559 add_mapping
: BoolProperty(
1560 name
="Add Mapping Nodes",
1561 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1565 def poll(cls
, context
):
1566 if nw_check(context
):
1567 space
= context
.space_data
1568 if space
.tree_type
== 'ShaderNodeTree':
1572 def execute(self
, context
):
1573 nodes
, links
= get_nodes_links(context
)
1575 texture_types
= get_texture_node_types()
1576 selected_nodes
= [n
for n
in nodes
if n
.select
]
1578 for node
in selected_nodes
:
1583 target_input
= node
.inputs
[0]
1584 for input in node
.inputs
:
1587 if not input.is_linked
:
1588 target_input
= input
1591 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1596 locx
= node
.location
.x
1597 locy
= node
.location
.y
- (input_index
* padding
)
1599 is_texture_node
= node
.rna_type
.identifier
in texture_types
1600 use_environment_texture
= node
.type == 'BACKGROUND'
1602 # Add an image texture before normal shader nodes.
1603 if not is_texture_node
:
1604 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1605 image_texture_node
= nodes
.new(image_texture_type
)
1606 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1607 image_texture_node
.location
= [locx
- x_offset
, locy
]
1608 nodes
.active
= image_texture_node
1609 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1611 # The mapping setup following this will connect to the first input of this image texture.
1612 target_input
= image_texture_node
.inputs
[0]
1616 if is_texture_node
or self
.add_mapping
:
1618 mapping_node
= nodes
.new('ShaderNodeMapping')
1619 x_offset
= x_offset
+ mapping_node
.width
+ padding
1620 mapping_node
.location
= [locx
- x_offset
, locy
]
1621 connect_sockets(mapping_node
.outputs
[0], target_input
)
1623 # Add Texture Coordinates node.
1624 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1625 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1626 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1628 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1629 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1630 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1631 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1636 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1637 bl_idname
= "node.nw_add_textures_for_principled"
1638 bl_label
= "Principled Texture Setup"
1639 bl_description
= "Add Texture Node Setup for Principled BSDF"
1640 bl_options
= {'REGISTER', 'UNDO'}
1642 directory
: StringProperty(
1646 description
='Folder to search in for image files'
1648 files
: CollectionProperty(
1649 type=bpy
.types
.OperatorFileListElement
,
1650 options
={'HIDDEN', 'SKIP_SAVE'}
1653 relative_path
: BoolProperty(
1654 name
='Relative Path',
1655 description
='Set the file path relative to the blend file, when possible',
1664 def draw(self
, context
):
1665 layout
= self
.layout
1666 layout
.alignment
= 'LEFT'
1668 layout
.prop(self
, 'relative_path')
1671 def poll(cls
, context
):
1673 if nw_check(context
):
1674 space
= context
.space_data
1675 if space
.tree_type
== 'ShaderNodeTree':
1679 def execute(self
, context
):
1680 # Check if everything is ok
1681 if not self
.directory
:
1682 self
.report({'INFO'}, 'No Folder Selected')
1683 return {'CANCELLED'}
1684 if not self
.files
[:]:
1685 self
.report({'INFO'}, 'No Files Selected')
1686 return {'CANCELLED'}
1688 nodes
, links
= get_nodes_links(context
)
1689 active_node
= nodes
.active
1690 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1691 self
.report({'INFO'}, 'Select Principled BSDF')
1692 return {'CANCELLED'}
1694 # Filter textures names for texturetypes in filenames
1695 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1696 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1697 normal_abbr
= tags
.normal
.split(' ')
1698 bump_abbr
= tags
.bump
.split(' ')
1699 gloss_abbr
= tags
.gloss
.split(' ')
1700 rough_abbr
= tags
.rough
.split(' ')
1702 ['Displacement', tags
.displacement
.split(' '), None],
1703 ['Base Color', tags
.base_color
.split(' '), None],
1704 ['Metallic', tags
.metallic
.split(' '), None],
1705 ['Specular IOR Level', tags
.specular
.split(' '), None],
1706 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1707 ['Normal', normal_abbr
+ bump_abbr
, None],
1708 ['Transmission Weight', tags
.transmission
.split(' '), None],
1709 ['Emission Color', tags
.emission
.split(' '), None],
1710 ['Alpha', tags
.alpha
.split(' '), None],
1711 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1714 match_files_to_socket_names(self
.files
, socketnames
)
1715 # Remove socketnames without found files
1716 socketnames
= [s
for s
in socketnames
if s
[2]
1717 and path
.exists(self
.directory
+ s
[2])]
1719 self
.report({'INFO'}, 'No matching images found')
1720 print('No matching images found')
1721 return {'CANCELLED'}
1723 # Don't override path earlier as os.path is used to check the absolute path
1724 import_path
= self
.directory
1725 if self
.relative_path
:
1726 if bpy
.data
.filepath
:
1728 import_path
= bpy
.path
.relpath(self
.directory
)
1733 print('\nMatched Textures:')
1738 roughness_node
= None
1739 for i
, sname
in enumerate(socketnames
):
1740 print(i
, sname
[0], sname
[2])
1742 # DISPLACEMENT NODES
1743 if sname
[0] == 'Displacement':
1744 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1745 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1746 disp_texture
.image
= img
1747 disp_texture
.label
= 'Displacement'
1748 if disp_texture
.image
:
1749 disp_texture
.image
.colorspace_settings
.is_data
= True
1751 # Add displacement offset nodes
1752 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
1753 # Align the Displacement node under the active Principled BSDF node
1754 disp_node
.location
= active_node
.location
+ Vector((100, -700))
1755 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
1757 # TODO Turn on true displacement in the material
1758 # Too complicated for now
1761 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
1763 if not output_node
[0].inputs
[2].is_linked
:
1764 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
1768 # AMBIENT OCCLUSION TEXTURE
1769 if sname
[0] == 'Ambient Occlusion':
1770 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
1771 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1772 ao_texture
.image
= img
1773 ao_texture
.label
= sname
[0]
1774 if ao_texture
.image
:
1775 ao_texture
.image
.colorspace_settings
.is_data
= True
1779 if not active_node
.inputs
[sname
[0]].is_linked
:
1780 # No texture node connected -> add texture node with new image
1781 texture_node
= nodes
.new(type='ShaderNodeTexImage')
1782 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
1783 texture_node
.image
= img
1786 if sname
[0] == 'Normal':
1787 # Test if new texture node is normal or bump map
1788 fname_components
= split_into_components(sname
[2])
1789 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
1790 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
1792 # If Normal add normal node in between
1793 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
1794 link
= connect_sockets(normal_node
.inputs
[1], texture_node
.outputs
[0])
1796 # If Bump add bump node in between
1797 normal_node
= nodes
.new(type='ShaderNodeBump')
1798 link
= connect_sockets(normal_node
.inputs
[2], texture_node
.outputs
[0])
1800 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
1801 normal_node_texture
= texture_node
1803 elif sname
[0] == 'Roughness':
1804 # Test if glossy or roughness map
1805 fname_components
= split_into_components(sname
[2])
1806 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
1807 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
1810 # If Roughness nothing to to
1811 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1814 # If Gloss Map add invert node
1815 invert_node
= nodes
.new(type='ShaderNodeInvert')
1816 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
1818 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
1819 roughness_node
= texture_node
1822 # This is a simple connection Texture --> Input slot
1823 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
1825 # Use non-color except for color inputs
1826 if sname
[0] not in ['Base Color', 'Emission Color'] and texture_node
.image
:
1827 texture_node
.image
.colorspace_settings
.is_data
= True
1830 # If already texture connected. add to node list for alignment
1831 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
1833 # This are all connected texture nodes
1834 texture_nodes
.append(texture_node
)
1835 texture_node
.label
= sname
[0]
1838 texture_nodes
.append(disp_texture
)
1841 # We want the ambient occlusion texture to be the top most texture node
1842 texture_nodes
.insert(0, ao_texture
)
1845 for i
, texture_node
in enumerate(texture_nodes
):
1846 offset
= Vector((-550, (i
* -280) + 200))
1847 texture_node
.location
= active_node
.location
+ offset
1850 # Extra alignment if normal node was added
1851 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
1854 # Alignment of invert node if glossy map
1855 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
1857 # Add texture input + mapping
1858 mapping
= nodes
.new(type='ShaderNodeMapping')
1859 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
1860 if len(texture_nodes
) > 1:
1861 # If more than one texture add reroute node in between
1862 reroute
= nodes
.new(type='NodeReroute')
1863 texture_nodes
.append(reroute
)
1864 tex_coords
= Vector((texture_nodes
[0].location
.x
,
1865 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
1866 reroute
.location
= tex_coords
+ Vector((-50, -120))
1867 for texture_node
in texture_nodes
:
1868 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
1869 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
1871 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
1873 # Connect texture_coordiantes to mapping node
1874 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
1875 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
1876 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
1878 # Create frame around tex coords and mapping
1879 frame
= nodes
.new(type='NodeFrame')
1880 frame
.label
= 'Mapping'
1881 mapping
.parent
= frame
1882 texture_input
.parent
= frame
1885 # Create frame around texture nodes
1886 frame
= nodes
.new(type='NodeFrame')
1887 frame
.label
= 'Textures'
1888 for tnode
in texture_nodes
:
1889 tnode
.parent
= frame
1893 active_node
.select
= False
1896 force_update(context
)
1900 class NWAddReroutes(Operator
, NWBase
):
1901 """Add Reroute Nodes and link them to outputs of selected nodes"""
1902 bl_idname
= "node.nw_add_reroutes"
1903 bl_label
= "Add Reroutes"
1904 bl_description
= "Add Reroutes to Outputs"
1905 bl_options
= {'REGISTER', 'UNDO'}
1907 option
: EnumProperty(
1910 ('ALL', 'to all', 'Add to all outputs'),
1911 ('LOOSE', 'to loose', 'Add only to loose outputs'),
1912 ('LINKED', 'to linked', 'Add only to linked outputs'),
1916 def execute(self
, context
):
1917 tree_type
= context
.space_data
.node_tree
.type
1918 option
= self
.option
1919 nodes
, links
= get_nodes_links(context
)
1920 # output valid when option is 'all' or when 'loose' output has no links
1922 post_select
= [] # nodes to be selected after execution
1923 # create reroutes and recreate links
1924 for node
in [n
for n
in nodes
if n
.select
]:
1929 # unhide 'REROUTE' nodes to avoid issues with location.y
1930 if node
.type == 'REROUTE':
1932 # Hack needed to calculate real width
1934 bpy
.ops
.node
.select_all(action
='DESELECT')
1935 helper
= nodes
.new('NodeReroute')
1936 helper
.select
= True
1938 # resize node and helper to zero. Then check locations to calculate width
1939 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
1940 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
1941 # restore node location
1942 node
.location
= x
, y
1945 # only helper is selected now
1946 bpy
.ops
.node
.delete()
1947 x
= node
.location
.x
+ width
+ 20.0
1948 if node
.type != 'REROUTE':
1952 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
1953 for out_i
, output
in enumerate(node
.outputs
):
1954 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
1955 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
1956 if node
.type != 'R_LAYERS':
1958 else: # if 'R_LAYERS' check if output represent used render pass
1959 node_scene
= node
.scene
1960 node_layer
= node
.layer
1961 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
1962 if output
.name
== 'Alpha':
1965 # check entries in global 'rl_outputs' variable
1966 for rlo
in rl_outputs
:
1967 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
1968 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
1971 valid
= ((option
== 'ALL') or
1972 (option
== 'LOOSE' and not output
.links
) or
1973 (option
== 'LINKED' and output
.links
))
1974 # Add reroutes only if valid, but offset location in all cases.
1976 n
= nodes
.new('NodeReroute')
1978 for link
in output
.links
:
1979 connect_sockets(n
.outputs
[0], link
.to_socket
)
1980 connect_sockets(output
, n
.inputs
[0])
1982 post_select
.append(n
)
1986 # disselect the node so that after execution of script only newly created nodes are selected
1988 # nicer reroutes distribution along y when node.hide
1990 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
1991 for reroute
in [r
for r
in nodes
if r
.select
]:
1992 reroute
.location
.y
-= y_translate
1993 for node
in post_select
:
1999 class NWLinkActiveToSelected(Operator
, NWBase
):
2000 """Link active node to selected nodes basing on various criteria"""
2001 bl_idname
= "node.nw_link_active_to_selected"
2002 bl_label
= "Link Active Node to Selected"
2003 bl_options
= {'REGISTER', 'UNDO'}
2005 replace
: BoolProperty()
2006 use_node_name
: BoolProperty()
2007 use_outputs_names
: BoolProperty()
2010 def poll(cls
, context
):
2012 if nw_check(context
):
2013 if context
.active_node
is not None:
2014 if context
.active_node
.select
:
2018 def execute(self
, context
):
2019 nodes
, links
= get_nodes_links(context
)
2020 replace
= self
.replace
2021 use_node_name
= self
.use_node_name
2022 use_outputs_names
= self
.use_outputs_names
2023 active
= nodes
.active
2024 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2025 outputs
= [] # Only usable outputs of active nodes will be stored here.
2026 for out
in active
.outputs
:
2027 if active
.type != 'R_LAYERS':
2030 # 'R_LAYERS' node type needs special handling.
2031 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2032 # Only outputs that represent used passes should be taken into account
2033 # Check if pass represented by output is used.
2034 # global 'rl_outputs' list will be used for that
2035 for rlo
in rl_outputs
:
2036 pass_used
= False # initial value. Will be set to True if pass is used
2037 if out
.name
== 'Alpha':
2038 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2040 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2041 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2042 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2046 doit
= True # Will be changed to False when links successfully added to previous output.
2049 for node
in selected
:
2050 dst_name
= node
.name
# Will be compared with src_name if needed.
2051 # When node has label - use it as dst_name
2053 dst_name
= node
.label
2054 valid
= True # Initial value. Will be changed to False if names don't match.
2055 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2057 # Set src_name to source node name or label
2058 src_name
= active
.name
2060 src_name
= active
.label
2061 elif use_outputs_names
:
2062 src_name
= (out
.name
, )
2063 for rlo
in rl_outputs
:
2064 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2065 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2066 if dst_name
not in src_name
:
2069 for input in node
.inputs
:
2070 if input.type == out
.type or node
.type == 'REROUTE':
2071 if replace
or not input.is_linked
:
2072 connect_sockets(out
, input)
2073 if not use_node_name
and not use_outputs_names
:
2080 class NWAlignNodes(Operator
, NWBase
):
2081 '''Align the selected nodes neatly in a row/column'''
2082 bl_idname
= "node.nw_align_nodes"
2083 bl_label
= "Align Nodes"
2084 bl_options
= {'REGISTER', 'UNDO'}
2085 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2087 def execute(self
, context
):
2088 nodes
, links
= get_nodes_links(context
)
2089 margin
= self
.margin
2093 if node
.select
and node
.type != 'FRAME':
2094 selection
.append(node
)
2096 # If no nodes are selected, align all nodes
2100 elif nodes
.active
in selection
:
2101 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2103 # Check if nodes should be laid out horizontally or vertically
2104 # use dimension to get center of node, not corner
2105 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2106 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2107 x_range
= max(x_locs
) - min(x_locs
)
2108 y_range
= max(y_locs
) - min(y_locs
)
2109 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2110 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2111 horizontal
= x_range
> y_range
2113 # Sort selection by location of node mid-point
2115 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2117 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2121 for node
in selection
:
2122 current_margin
= margin
2123 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2126 node
.location
.x
= current_pos
2127 current_pos
+= current_margin
+ node
.dimensions
.x
2128 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2130 node
.location
.y
= current_pos
2131 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2132 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2134 # If active node is selected, center nodes around it
2135 if active_loc
is not None:
2136 active_loc_diff
= active_loc
- nodes
.active
.location
2137 for node
in selection
:
2138 node
.location
+= active_loc_diff
2139 else: # Position nodes centered around where they used to be
2140 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2141 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2142 new_mid
= (max(locs
) + min(locs
)) / 2
2143 for node
in selection
:
2145 node
.location
.x
+= (mid_x
- new_mid
)
2147 node
.location
.y
+= (mid_y
- new_mid
)
2152 class NWSelectParentChildren(Operator
, NWBase
):
2153 bl_idname
= "node.nw_select_parent_child"
2154 bl_label
= "Select Parent or Children"
2155 bl_options
= {'REGISTER', 'UNDO'}
2157 option
: EnumProperty(
2160 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2161 ('CHILD', 'Select Children', 'Select members of selected frame'),
2165 def execute(self
, context
):
2166 nodes
, links
= get_nodes_links(context
)
2167 option
= self
.option
2168 selected
= [node
for node
in nodes
if node
.select
]
2169 if option
== 'PARENT':
2170 for sel
in selected
:
2173 parent
.select
= True
2174 else: # option == 'CHILD'
2175 for sel
in selected
:
2176 children
= [node
for node
in nodes
if node
.parent
== sel
]
2177 for kid
in children
:
2183 class NWDetachOutputs(Operator
, NWBase
):
2184 """Detach outputs of selected node leaving inputs linked"""
2185 bl_idname
= "node.nw_detach_outputs"
2186 bl_label
= "Detach Outputs"
2187 bl_options
= {'REGISTER', 'UNDO'}
2189 def execute(self
, context
):
2190 nodes
, links
= get_nodes_links(context
)
2191 selected
= context
.selected_nodes
2192 bpy
.ops
.node
.duplicate_move_keep_inputs()
2193 new_nodes
= context
.selected_nodes
2194 bpy
.ops
.node
.select_all(action
="DESELECT")
2195 for node
in selected
:
2197 bpy
.ops
.node
.delete_reconnect()
2198 for new_node
in new_nodes
:
2199 new_node
.select
= True
2200 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2205 class NWLinkToOutputNode(Operator
):
2206 """Link to Composite node or Material Output node"""
2207 bl_idname
= "node.nw_link_out"
2208 bl_label
= "Connect to Output"
2209 bl_options
= {'REGISTER', 'UNDO'}
2212 def poll(cls
, context
):
2214 if nw_check(context
):
2215 if context
.active_node
is not None:
2216 for out
in context
.active_node
.outputs
:
2217 if is_visible_socket(out
):
2222 def execute(self
, context
):
2223 nodes
, links
= get_nodes_links(context
)
2224 active
= nodes
.active
2226 tree_type
= context
.space_data
.tree_type
2227 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2228 'WORLD': 'ShaderNodeOutputWorld',
2229 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2231 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2232 'CompositorNodeTree': 'CompositorNodeComposite',
2233 'TextureNodeTree': 'TextureNodeOutput',
2234 'GeometryNodeTree': 'NodeGroupOutput',
2237 # check whether the node is an output node and,
2238 # if supported, whether it's the active one
2239 if node
.rna_type
.identifier
== output_type \
2240 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2244 else: # No output node exists
2245 bpy
.ops
.node
.select_all(action
="DESELECT")
2246 output_node
= nodes
.new(output_type
)
2247 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2248 output_node
.location
.y
= active
.location
.y
2251 for i
, output
in enumerate(active
.outputs
):
2252 if is_visible_socket(output
):
2255 for i
, output
in enumerate(active
.outputs
):
2256 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2261 if tree_type
== 'ShaderNodeTree':
2262 if active
.outputs
[output_index
].name
== 'Volume':
2264 elif active
.outputs
[output_index
].name
== 'Displacement':
2266 elif tree_type
== 'GeometryNodeTree':
2267 if active
.outputs
[output_index
].type != 'GEOMETRY':
2268 return {'CANCELLED'}
2269 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2271 force_update(context
) # viewport render does not update
2276 class NWMakeLink(Operator
, NWBase
):
2277 """Make a link from one socket to another"""
2278 bl_idname
= 'node.nw_make_link'
2279 bl_label
= 'Make Link'
2280 bl_options
= {'REGISTER', 'UNDO'}
2281 from_socket
: IntProperty()
2282 to_socket
: IntProperty()
2284 def execute(self
, context
):
2285 nodes
, links
= get_nodes_links(context
)
2287 n1
= nodes
[context
.scene
.NWLazySource
]
2288 n2
= nodes
[context
.scene
.NWLazyTarget
]
2290 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2292 force_update(context
)
2297 class NWCallInputsMenu(Operator
, NWBase
):
2298 """Link from this output"""
2299 bl_idname
= 'node.nw_call_inputs_menu'
2300 bl_label
= 'Make Link'
2301 bl_options
= {'REGISTER', 'UNDO'}
2302 from_socket
: IntProperty()
2304 def execute(self
, context
):
2305 nodes
, links
= get_nodes_links(context
)
2307 context
.scene
.NWSourceSocket
= self
.from_socket
2309 n1
= nodes
[context
.scene
.NWLazySource
]
2310 n2
= nodes
[context
.scene
.NWLazyTarget
]
2311 if len(n2
.inputs
) > 1:
2312 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2313 elif len(n2
.inputs
) == 1:
2314 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2318 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2319 """Add an Image Sequence"""
2320 bl_idname
= 'node.nw_add_sequence'
2321 bl_label
= 'Import Image Sequence'
2322 bl_options
= {'REGISTER', 'UNDO'}
2324 directory
: StringProperty(
2327 filename
: StringProperty(
2330 files
: CollectionProperty(
2331 type=bpy
.types
.OperatorFileListElement
,
2332 options
={'HIDDEN', 'SKIP_SAVE'}
2334 relative_path
: BoolProperty(
2335 name
='Relative Path',
2336 description
='Set the file path relative to the blend file, when possible',
2340 def draw(self
, context
):
2341 layout
= self
.layout
2342 layout
.alignment
= 'LEFT'
2344 layout
.prop(self
, 'relative_path')
2346 def execute(self
, context
):
2347 nodes
, links
= get_nodes_links(context
)
2348 directory
= self
.directory
2349 filename
= self
.filename
2351 tree
= context
.space_data
.node_tree
2354 # print ("\nDIR:", directory)
2355 # print ("FN:", filename)
2356 # print ("Fs:", list(f.name for f in files), '\n')
2358 if tree
.type == 'SHADER':
2359 node_type
= "ShaderNodeTexImage"
2360 elif tree
.type == 'COMPOSITING':
2361 node_type
= "CompositorNodeImage"
2363 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2364 return {'CANCELLED'}
2366 if not files
[0].name
and not filename
:
2367 self
.report({'ERROR'}, "No file chosen")
2368 return {'CANCELLED'}
2369 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2370 # User has selected multiple files without an active one, or the active one is non-existent
2371 filename
= files
[0].name
2373 if not path
.exists(directory
+ filename
):
2374 self
.report({'ERROR'}, filename
+ " does not exist!")
2375 return {'CANCELLED'}
2377 without_ext
= '.'.join(filename
.split('.')[:-1])
2379 # if last digit isn't a number, it's not a sequence
2380 if not without_ext
[-1].isdigit():
2381 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2382 return {'CANCELLED'}
2384 extension
= filename
.split('.')[-1]
2385 reverse
= without_ext
[::-1] # reverse string
2388 for char
in reverse
:
2394 without_num
= without_ext
[:count_numbers
* -1]
2396 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2398 num_frames
= len(files
)
2400 nodes_list
= [node
for node
in nodes
]
2402 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2403 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2407 yloc
+= node_mid_pt(node
, 'y')
2408 yloc
= yloc
/ len(nodes
)
2413 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2415 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2417 node
.label
= name_with_hashes
2419 filepath
= directory
+ (without_ext
+ '.' + extension
)
2420 if self
.relative_path
:
2421 if bpy
.data
.filepath
:
2423 filepath
= bpy
.path
.relpath(filepath
)
2427 img
= bpy
.data
.images
.load(filepath
)
2428 img
.source
= 'SEQUENCE'
2429 img
.name
= name_with_hashes
2431 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2432 # separate the number from the file name of the first file
2433 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2434 image_user
.frame_duration
= num_frames
2439 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2440 """Add multiple images at once"""
2441 bl_idname
= 'node.nw_add_multiple_images'
2442 bl_label
= 'Open Selected Images'
2443 bl_options
= {'REGISTER', 'UNDO'}
2444 directory
: StringProperty(
2447 files
: CollectionProperty(
2448 type=bpy
.types
.OperatorFileListElement
,
2449 options
={'HIDDEN', 'SKIP_SAVE'}
2452 def execute(self
, context
):
2453 nodes
, links
= get_nodes_links(context
)
2455 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2457 if context
.space_data
.node_tree
.type == 'SHADER':
2458 node_type
= "ShaderNodeTexImage"
2459 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2460 node_type
= "CompositorNodeImage"
2462 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2463 return {'CANCELLED'}
2466 for f
in self
.files
:
2469 node
= nodes
.new(node_type
)
2470 new_nodes
.append(node
)
2473 node
.location
.x
= xloc
2474 node
.location
.y
= yloc
2477 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2480 # shift new nodes up to center of tree
2481 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2483 if node
in new_nodes
:
2485 node
.location
.y
+= (list_size
/ 2)
2491 class NWViewerFocus(bpy
.types
.Operator
):
2492 """Set the viewer tile center to the mouse position"""
2493 bl_idname
= "node.nw_viewer_focus"
2494 bl_label
= "Viewer Focus"
2496 x
: bpy
.props
.IntProperty()
2497 y
: bpy
.props
.IntProperty()
2500 def poll(cls
, context
):
2501 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
2503 def execute(self
, context
):
2506 def invoke(self
, context
, event
):
2507 render
= context
.scene
.render
2508 space
= context
.space_data
2509 percent
= render
.resolution_percentage
* 0.01
2511 nodes
, links
= get_nodes_links(context
)
2512 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2515 mlocx
= event
.mouse_region_x
2516 mlocy
= event
.mouse_region_y
2517 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2519 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2520 region_x
= context
.region
.width
2521 region_y
= context
.region
.height
2523 region_center_x
= context
.region
.width
/ 2
2524 region_center_y
= context
.region
.height
/ 2
2526 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2527 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2529 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2530 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2532 margin_x
= region_center_x
- backdrop_center_x
2533 margin_y
= region_center_y
- backdrop_center_y
2535 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2536 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2538 for node
in viewers
:
2539 node
.center_x
= abs_mouse_x
2540 node
.center_y
= abs_mouse_y
2542 return {'PASS_THROUGH'}
2544 return self
.execute(context
)
2547 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2548 """Save the current viewer node to an image file"""
2549 bl_idname
= "node.nw_save_viewer"
2550 bl_label
= "Save This Image"
2551 filepath
: StringProperty(subtype
="FILE_PATH")
2552 filename_ext
: EnumProperty(
2554 description
="Choose the file format to save to",
2555 items
=(('.bmp', "BMP", ""),
2556 ('.rgb', 'IRIS', ""),
2557 ('.png', 'PNG', ""),
2558 ('.jpg', 'JPEG', ""),
2559 ('.jp2', 'JPEG2000', ""),
2560 ('.tga', 'TARGA', ""),
2561 ('.cin', 'CINEON', ""),
2562 ('.dpx', 'DPX', ""),
2563 ('.exr', 'OPEN_EXR', ""),
2564 ('.hdr', 'HDR', ""),
2565 ('.tif', 'TIFF', "")),
2570 def poll(cls
, context
):
2572 if nw_check(context
):
2573 if context
.space_data
.tree_type
== 'CompositorNodeTree':
2574 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
2575 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
2579 def execute(self
, context
):
2596 basename
, ext
= path
.splitext(fp
)
2597 old_render_format
= context
.scene
.render
.image_settings
.file_format
2598 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2599 context
.area
.type = "IMAGE_EDITOR"
2600 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2601 context
.area
.spaces
[0].image
.save_render(fp
)
2602 context
.area
.type = "NODE_EDITOR"
2603 context
.scene
.render
.image_settings
.file_format
= old_render_format
2607 class NWResetNodes(bpy
.types
.Operator
):
2608 """Reset Nodes in Selection"""
2609 bl_idname
= "node.nw_reset_nodes"
2610 bl_label
= "Reset Nodes"
2611 bl_options
= {'REGISTER', 'UNDO'}
2614 def poll(cls
, context
):
2615 space
= context
.space_data
2616 return space
.type == 'NODE_EDITOR'
2618 def execute(self
, context
):
2619 node_active
= context
.active_node
2620 node_selected
= context
.selected_nodes
2621 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2623 # Check if one node is selected at least
2624 if not (len(node_selected
) > 0):
2625 self
.report({'ERROR'}, "1 node must be selected at least")
2626 return {'CANCELLED'}
2628 active_node_name
= node_active
.name
if node_active
.select
else None
2629 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2631 # Create output lists
2632 selected_node_names
= [n
.name
for n
in node_selected
]
2635 # Reset all valid children in a frame
2636 node_active_is_frame
= False
2637 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2638 node_tree
= node_active
.id_data
2639 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2641 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2642 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2643 node_active_is_frame
= True
2645 # Check if valid nodes in selection
2646 if not (len(valid_nodes
) > 0):
2647 # Check for frames only
2648 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2649 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2650 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2652 self
.report({'ERROR'}, "No valid node(s) in selection")
2653 return {'CANCELLED'}
2655 # Report nodes that are not valid
2656 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2657 valid_node_names
= [n
.name
for n
in valid_nodes
]
2658 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2659 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2661 # Deselect all nodes
2662 for i
in node_selected
:
2665 # Run through all valid nodes
2666 for node
in valid_nodes
:
2668 parent
= node
.parent
if node
.parent
else None
2669 node_loc
= [node
.location
.x
, node
.location
.y
]
2671 node_tree
= node
.id_data
2672 props_to_copy
= 'bl_idname name location height width'.split(' ')
2675 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2676 for i
in (i
for i
in mappings
if i
.is_linked
):
2678 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2680 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2682 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2683 props_to_copy
.pop(0)
2685 for prop
in props_to_copy
:
2686 setattr(new_node
, prop
, props
[prop
])
2688 nodes
= node_tree
.nodes
2690 new_node
.name
= props
['name']
2693 new_node
.parent
= parent
2694 new_node
.location
= node_loc
2696 for str_from
, str_to
in reconnections
:
2697 connect_sockets(eval(str_from
), eval(str_to
))
2699 new_node
.select
= False
2700 success_names
.append(new_node
.name
)
2702 # Reselect all nodes
2703 if selected_node_names
and node_active_is_frame
is False:
2704 for i
in selected_node_names
:
2705 node_tree
.nodes
[i
].select
= True
2707 if active_node_name
is not None:
2708 node_tree
.nodes
[active_node_name
].select
= True
2709 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2711 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2733 NWAddPrincipledSetup
,
2735 NWLinkActiveToSelected
,
2737 NWSelectParentChildren
,
2743 NWAddMultipleImages
,
2751 from bpy
.utils
import register_class
2757 from bpy
.utils
import unregister_class
2760 unregister_class(cls
)