1 # SPDX-FileCopyrightText: 2013-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 from bpy
.types
import Operator
8 from bpy
.props
import (
17 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
18 from bpy_extras
.node_utils
import connect_sockets
19 from mathutils
import Vector
23 from itertools
import chain
25 from .interface
import NWConnectionListInputs
, NWConnectionListOutputs
27 from .utils
.constants
import blend_types
, geo_combine_operations
, operations
, navs
, get_texture_node_types
, rl_outputs
28 from .utils
.draw
import draw_callback_nodeoutline
29 from .utils
.paths
import match_files_to_socket_names
, split_into_components
30 from .utils
.nodes
import (node_mid_pt
, autolink
, node_at_pos
, get_active_tree
, get_nodes_links
, is_viewer_socket
,
31 is_viewer_link
, get_group_output_node
, get_output_location
, force_update
, get_internal_socket
,
32 nw_check
, NWBase
, get_first_enabled_output
, is_visible_socket
, viewer_socket_name
)
35 class NWLazyMix(Operator
, NWBase
):
36 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
37 bl_idname
= "node.nw_lazy_mix"
38 bl_label
= "Mix Nodes"
39 bl_options
= {'REGISTER', 'UNDO'}
41 def modal(self
, context
, event
):
42 context
.area
.tag_redraw()
43 nodes
, links
= get_nodes_links(context
)
46 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
49 if not context
.scene
.NWBusyDrawing
:
50 node1
= node_at_pos(nodes
, context
, event
)
52 context
.scene
.NWBusyDrawing
= node1
.name
54 if context
.scene
.NWBusyDrawing
!= 'STOP':
55 node1
= nodes
[context
.scene
.NWBusyDrawing
]
57 context
.scene
.NWLazySource
= node1
.name
58 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
60 if event
.type == 'MOUSEMOVE':
61 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
63 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
64 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
65 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
68 node2
= node_at_pos(nodes
, context
, event
)
70 context
.scene
.NWBusyDrawing
= node2
.name
82 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
84 context
.scene
.NWBusyDrawing
= ""
87 elif event
.type == 'ESC':
89 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
92 return {'RUNNING_MODAL'}
94 def invoke(self
, context
, event
):
95 if context
.area
.type == 'NODE_EDITOR':
96 # the arguments we pass the the callback
97 args
= (self
, context
, 'MIX')
98 # Add the region OpenGL drawing callback
99 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
100 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
101 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
105 context
.window_manager
.modal_handler_add(self
)
106 return {'RUNNING_MODAL'}
108 self
.report({'WARNING'}, "View3D not found, cannot run operator")
112 class NWLazyConnect(Operator
, NWBase
):
113 """Connect two nodes without clicking a specific socket (automatically determined"""
114 bl_idname
= "node.nw_lazy_connect"
115 bl_label
= "Lazy Connect"
116 bl_options
= {'REGISTER', 'UNDO'}
117 with_menu
: BoolProperty()
119 def modal(self
, context
, event
):
120 context
.area
.tag_redraw()
121 nodes
, links
= get_nodes_links(context
)
124 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
127 if not context
.scene
.NWBusyDrawing
:
128 node1
= node_at_pos(nodes
, context
, event
)
130 context
.scene
.NWBusyDrawing
= node1
.name
132 if context
.scene
.NWBusyDrawing
!= 'STOP':
133 node1
= nodes
[context
.scene
.NWBusyDrawing
]
135 context
.scene
.NWLazySource
= node1
.name
136 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
138 if event
.type == 'MOUSEMOVE':
139 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
141 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
142 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
143 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
146 node2
= node_at_pos(nodes
, context
, event
)
148 context
.scene
.NWBusyDrawing
= node2
.name
161 original_sel
.append(node
)
163 original_unsel
.append(node
)
167 # link_success = autolink(node1, node2, links)
169 if len(node1
.outputs
) > 1 and node2
.inputs
:
170 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
171 elif len(node1
.outputs
) == 1:
172 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
174 link_success
= autolink(node1
, node2
, links
)
176 for node
in original_sel
:
178 for node
in original_unsel
:
182 force_update(context
)
183 context
.scene
.NWBusyDrawing
= ""
186 elif event
.type == 'ESC':
187 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
190 return {'RUNNING_MODAL'}
192 def invoke(self
, context
, event
):
193 if context
.area
.type == 'NODE_EDITOR':
194 nodes
, links
= get_nodes_links(context
)
195 node
= node_at_pos(nodes
, context
, event
)
197 context
.scene
.NWBusyDrawing
= node
.name
199 # the arguments we pass the the callback
203 args
= (self
, context
, mode
)
204 # Add the region OpenGL drawing callback
205 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
206 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(
207 draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
211 context
.window_manager
.modal_handler_add(self
)
212 return {'RUNNING_MODAL'}
214 self
.report({'WARNING'}, "View3D not found, cannot run operator")
218 class NWDeleteUnused(Operator
, NWBase
):
219 """Delete all nodes whose output is not used"""
220 bl_idname
= 'node.nw_del_unused'
221 bl_label
= 'Delete Unused Nodes'
222 bl_options
= {'REGISTER', 'UNDO'}
224 delete_muted
: BoolProperty(
226 description
="Delete (but reconnect, like Ctrl-X) all muted nodes",
228 delete_frames
: BoolProperty(
229 name
="Delete Empty Frames",
230 description
="Delete all frames that have no nodes inside them",
233 def is_unused_node(self
, node
):
234 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE',
235 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT',
236 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
237 if node
.type in end_types
:
240 for output
in node
.outputs
:
246 def poll(cls
, context
):
248 if nw_check(context
):
249 if context
.space_data
.node_tree
.nodes
:
253 def execute(self
, context
):
254 nodes
, links
= get_nodes_links(context
)
260 selection
.append(node
.name
)
266 temp_deleted_nodes
= []
267 del_unused_iterations
= len(nodes
)
268 for it
in range(0, del_unused_iterations
):
269 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
271 if self
.is_unused_node(node
):
273 deleted_nodes
.append(node
.name
)
274 bpy
.ops
.node
.delete()
276 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
279 if self
.delete_frames
:
287 frames_in_use
.append(node
.parent
)
289 if node
.type == 'FRAME' and node
not in frames_in_use
:
292 repeat
= True # repeat for nested frames
294 if node
not in frames_in_use
:
296 deleted_nodes
.append(node
.name
)
297 bpy
.ops
.node
.delete()
299 if self
.delete_muted
:
303 deleted_nodes
.append(node
.name
)
304 bpy
.ops
.node
.delete_reconnect()
306 # get unique list of deleted nodes (iterations would count the same node more than once)
307 deleted_nodes
= list(set(deleted_nodes
))
308 for n
in deleted_nodes
:
309 self
.report({'INFO'}, "Node " + n
+ " deleted")
310 num_deleted
= len(deleted_nodes
)
315 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
317 self
.report({'INFO'}, "Nothing deleted")
320 nodes
, links
= get_nodes_links(context
)
322 if node
.name
in selection
:
326 def invoke(self
, context
, event
):
327 return context
.window_manager
.invoke_confirm(self
, event
)
330 class NWSwapLinks(Operator
, NWBase
):
331 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
332 bl_idname
= 'node.nw_swap_links'
333 bl_label
= 'Swap Links'
334 bl_options
= {'REGISTER', 'UNDO'}
337 def poll(cls
, context
):
339 if nw_check(context
):
340 if context
.selected_nodes
:
341 valid
= len(context
.selected_nodes
) <= 2
344 def execute(self
, context
):
345 nodes
, links
= get_nodes_links(context
)
346 selected_nodes
= context
.selected_nodes
347 n1
= selected_nodes
[0]
350 if len(selected_nodes
) == 2:
351 n2
= selected_nodes
[1]
352 if n1
.outputs
and n2
.outputs
:
357 for output
in n1
.outputs
:
359 for link
in output
.links
:
360 n1_outputs
.append([out_index
, link
.to_socket
])
365 for output
in n2
.outputs
:
367 for link
in output
.links
:
368 n2_outputs
.append([out_index
, link
.to_socket
])
372 for connection
in n1_outputs
:
374 connect_sockets(n2
.outputs
[connection
[0]], connection
[1])
376 self
.report({'WARNING'},
377 "Some connections have been lost due to differing numbers of output sockets")
378 for connection
in n2_outputs
:
380 connect_sockets(n1
.outputs
[connection
[0]], connection
[1])
382 self
.report({'WARNING'},
383 "Some connections have been lost due to differing numbers of output sockets")
385 if n1
.outputs
or n2
.outputs
:
386 self
.report({'WARNING'}, "One of the nodes has no outputs!")
388 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
391 elif len(selected_nodes
) == 1:
392 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
393 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
399 if i1
.is_linked
and not i1
.is_multi_input
:
402 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
404 types
.append([i1
, similar_types
, i
])
406 types
.sort(key
=lambda k
: k
[1], reverse
=True)
412 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
414 i1f
= pair
[0].links
[0].from_socket
415 i1t
= pair
[0].links
[0].to_socket
416 i2f
= pair
[1].links
[0].from_socket
417 i2t
= pair
[1].links
[0].to_socket
418 connect_sockets(i1f
, i2t
)
419 connect_sockets(i2f
, i1t
)
422 fs
= t
[0].links
[0].from_socket
424 links
.remove(t
[0].links
[0])
425 if i
+ 1 == len(n1
.inputs
):
428 while n1
.inputs
[i
].is_linked
:
430 connect_sockets(fs
, n1
.inputs
[i
])
431 elif len(types
) == 2:
432 i1f
= types
[0][0].links
[0].from_socket
433 i1t
= types
[0][0].links
[0].to_socket
434 i2f
= types
[1][0].links
[0].from_socket
435 i2t
= types
[1][0].links
[0].to_socket
436 connect_sockets(i1f
, i2t
)
437 connect_sockets(i2f
, i1t
)
440 self
.report({'WARNING'}, "This node has no input connections to swap!")
442 self
.report({'WARNING'}, "This node has no inputs to swap!")
444 force_update(context
)
448 class NWResetBG(Operator
, NWBase
):
449 """Reset the zoom and position of the background image"""
450 bl_idname
= 'node.nw_bg_reset'
451 bl_label
= 'Reset Backdrop'
452 bl_options
= {'REGISTER', 'UNDO'}
455 def poll(cls
, context
):
457 if nw_check(context
):
458 snode
= context
.space_data
459 valid
= snode
.tree_type
== 'CompositorNodeTree'
462 def execute(self
, context
):
463 context
.space_data
.backdrop_zoom
= 1
464 context
.space_data
.backdrop_offset
[0] = 0
465 context
.space_data
.backdrop_offset
[1] = 0
469 class NWAddAttrNode(Operator
, NWBase
):
470 """Add an Attribute node with this name"""
471 bl_idname
= 'node.nw_add_attr_node'
472 bl_label
= 'Add UV map'
473 bl_options
= {'REGISTER', 'UNDO'}
475 attr_name
: StringProperty()
477 def execute(self
, context
):
478 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
479 nodes
, links
= get_nodes_links(context
)
480 nodes
.active
.attribute_name
= self
.attr_name
484 class NWPreviewNode(Operator
, NWBase
):
485 bl_idname
= "node.nw_preview_node"
486 bl_label
= "Preview Node"
487 bl_description
= "Connect active node to the Node Group output or the Material Output"
488 bl_options
= {'REGISTER', 'UNDO'}
490 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
491 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
492 run_in_geometry_nodes
: BoolProperty(default
=True)
495 self
.shader_output_type
= ""
496 self
.shader_output_ident
= ""
499 def poll(cls
, context
):
500 if nw_check(context
):
501 space
= context
.space_data
502 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
503 if context
.active_node
:
504 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
511 def get_output_sockets(cls
, node_tree
):
512 return [item
for item
in node_tree
.interface
.items_tree
if item
.item_type
== 'SOCKET' and item
.in_out
in {'OUTPUT', 'BOTH'}]
514 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
515 # check if a viewer output already exists in a node group otherwise create
516 if hasattr(node
, "node_tree"):
518 output_sockets
= self
.get_output_sockets(node
.node_tree
)
519 if len(output_sockets
):
521 for socket
in output_sockets
:
522 if is_viewer_socket(socket
) and socket
.socket_type
== socket_type
:
523 # if viewer output is already used but leads to the same socket we can still use it
524 is_used
= self
.is_socket_used_other_mats(socket
)
526 if connect_socket
is None:
528 groupout
= get_group_output_node(node
.node_tree
)
529 groupout_input
= groupout
.inputs
[i
]
530 links
= groupout_input
.links
531 if connect_socket
not in [link
.from_socket
for link
in links
]:
533 viewer_socket
= socket
537 if not viewer_socket
and free_socket
:
538 viewer_socket
= free_socket
540 if not viewer_socket
:
541 # create viewer socket
542 viewer_socket
= node
.node_tree
.interface
.new_socket(viewer_socket_name
, in_out
='OUTPUT', socket_type
=socket_type
)
543 viewer_socket
.NWViewerSocket
= True
546 def init_shader_variables(self
, space
, shader_type
):
547 if shader_type
== 'OBJECT':
548 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
549 self
.shader_output_type
= "OUTPUT_MATERIAL"
550 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
552 self
.shader_output_type
= "OUTPUT_LIGHT"
553 self
.shader_output_ident
= "ShaderNodeOutputLight"
555 elif shader_type
== 'WORLD':
556 self
.shader_output_type
= "OUTPUT_WORLD"
557 self
.shader_output_ident
= "ShaderNodeOutputWorld"
559 def get_shader_output_node(self
, tree
):
560 for node
in tree
.nodes
:
561 if node
.type == self
.shader_output_type
and node
.is_active_output
:
565 def ensure_group_output(cls
, tree
):
566 # check if a group output node exists otherwise create
567 groupout
= get_group_output_node(tree
)
569 groupout
= tree
.nodes
.new('NodeGroupOutput')
570 loc_x
, loc_y
= get_output_location(tree
)
571 groupout
.location
.x
= loc_x
572 groupout
.location
.y
= loc_y
573 groupout
.select
= False
574 # So that we don't keep on adding new group outputs
575 groupout
.is_active_output
= True
579 def search_sockets(cls
, node
, sockets
, index
=None):
580 # recursively scan nodes for viewer sockets and store in list
581 for i
, input_socket
in enumerate(node
.inputs
):
582 if index
and i
!= index
:
584 if len(input_socket
.links
):
585 link
= input_socket
.links
[0]
586 next_node
= link
.from_node
587 external_socket
= link
.from_socket
588 if hasattr(next_node
, "node_tree"):
589 for socket_index
, socket
in enumerate(next_node
.node_tree
.interface
.items_tree
):
590 if socket
.identifier
== external_socket
.identifier
:
592 if is_viewer_socket(socket
) and socket
not in sockets
:
593 sockets
.append(socket
)
594 # continue search inside of node group but restrict socket to where we came from
595 groupout
= get_group_output_node(next_node
.node_tree
)
596 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
599 def scan_nodes(cls
, tree
, sockets
):
600 # get all viewer sockets in a material tree
601 for node
in tree
.nodes
:
602 if hasattr(node
, "node_tree"):
603 if node
.node_tree
is None:
605 for socket
in cls
.get_output_sockets(node
.node_tree
):
606 if is_viewer_socket(socket
) and (socket
not in sockets
):
607 sockets
.append(socket
)
608 cls
.scan_nodes(node
.node_tree
, sockets
)
611 def remove_socket(cls
, tree
, socket
):
612 interface
= tree
.interface
613 interface
.remove(socket
)
614 interface
.active_index
= min(interface
.active_index
, len(interface
.items_tree
) - 1)
616 def link_leads_to_used_socket(self
, link
):
617 # return True if link leads to a socket that is already used in this material
618 socket
= get_internal_socket(link
.to_socket
)
619 return (socket
and self
.is_socket_used_active_mat(socket
))
621 def is_socket_used_active_mat(self
, socket
):
622 # ensure used sockets in active material is calculated and check given socket
623 if not hasattr(self
, "used_viewer_sockets_active_mat"):
624 self
.used_viewer_sockets_active_mat
= []
625 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
627 self
.search_sockets(materialout
, self
.used_viewer_sockets_active_mat
)
628 return socket
in self
.used_viewer_sockets_active_mat
630 def is_socket_used_other_mats(self
, socket
):
631 # ensure used sockets in other materials are calculated and check given socket
632 if not hasattr(self
, "used_viewer_sockets_other_mats"):
633 self
.used_viewer_sockets_other_mats
= []
634 for mat
in bpy
.data
.materials
:
635 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
638 materialout
= self
.get_shader_output_node(mat
.node_tree
)
640 self
.search_sockets(materialout
, self
.used_viewer_sockets_other_mats
)
641 return socket
in self
.used_viewer_sockets_other_mats
643 def invoke(self
, context
, event
):
644 space
= context
.space_data
645 # Ignore operator when running in wrong context.
646 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
647 return {'PASS_THROUGH'}
649 shader_type
= space
.shader_type
650 self
.init_shader_variables(space
, shader_type
)
651 mlocx
= event
.mouse_region_x
652 mlocy
= event
.mouse_region_y
653 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
654 if 'FINISHED' in select_node
: # only run if mouse click is on a node
655 active_tree
, path_to_tree
= get_active_tree(context
)
656 nodes
, links
= active_tree
.nodes
, active_tree
.links
657 base_node_tree
= space
.node_tree
658 active
= nodes
.active
660 # For geometry node trees we just connect to the group output
661 if space
.tree_type
== "GeometryNodeTree":
664 for out
in active
.outputs
:
665 if is_visible_socket(out
):
674 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
675 self
.scan_nodes(base_node_tree
, delete_sockets
)
677 # Find (or create if needed) the output of this node tree
678 geometryoutput
= self
.ensure_group_output(base_node_tree
)
680 # Analyze outputs, make links
683 for i
, out
in enumerate(active
.outputs
):
684 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
685 valid_outputs
.append(i
)
687 out_i
= valid_outputs
[0] # Start index of node's outputs
688 for i
, valid_i
in enumerate(valid_outputs
):
689 for out_link
in active
.outputs
[valid_i
].links
:
690 if is_viewer_link(out_link
, geometryoutput
):
691 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
692 if i
< len(valid_outputs
) - 1:
693 out_i
= valid_outputs
[i
+ 1]
695 out_i
= valid_outputs
[0]
697 make_links
= [] # store sockets for new links
699 # If there is no 'GEOMETRY' output type - We can't preview the node
702 socket_type
= 'GEOMETRY'
703 # Find an input socket of the output of type geometry
704 geometryoutindex
= None
705 for i
, inp
in enumerate(geometryoutput
.inputs
):
706 if inp
.type == socket_type
:
709 if geometryoutindex
is None:
710 # Create geometry socket
711 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
712 geometryoutindex
= len(geometryoutput
.inputs
) - 1
714 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
715 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
716 for li_from
, li_to
in make_links
:
717 connect_sockets(li_from
, li_to
)
718 tree
= base_node_tree
719 link_end
= output_socket
720 while tree
.nodes
.active
!= active
:
721 node
= tree
.nodes
.active
722 viewer_socket
= self
.ensure_viewer_socket(
723 node
, 'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
724 link_start
= node
.outputs
[viewer_socket_name
]
725 node_socket
= viewer_socket
726 if node_socket
in delete_sockets
:
727 delete_sockets
.remove(node_socket
)
728 connect_sockets(link_start
, link_end
)
730 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[viewer_socket_name
]
731 tree
= tree
.nodes
.active
.node_tree
732 connect_sockets(active
.outputs
[out_i
], link_end
)
735 for socket
in delete_sockets
:
736 tree
= socket
.id_data
737 self
.remove_socket(tree
, socket
)
739 nodes
.active
= active
741 force_update(context
)
744 # What follows is code for the shader editor
747 for out
in active
.outputs
:
748 if is_visible_socket(out
):
752 # get material_output node
753 materialout
= None # placeholder node
756 # scan through all nodes in tree including nodes inside of groups to find viewer sockets
757 self
.scan_nodes(base_node_tree
, delete_sockets
)
759 materialout
= self
.get_shader_output_node(base_node_tree
)
761 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
762 materialout
.location
= get_output_location(base_node_tree
)
763 materialout
.select
= False
767 for i
, out
in enumerate(active
.outputs
):
768 if is_visible_socket(out
):
769 valid_outputs
.append(i
)
771 out_i
= valid_outputs
[0] # Start index of node's outputs
772 for i
, valid_i
in enumerate(valid_outputs
):
773 for out_link
in active
.outputs
[valid_i
].links
:
774 if is_viewer_link(out_link
, materialout
):
775 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
776 if i
< len(valid_outputs
) - 1:
777 out_i
= valid_outputs
[i
+ 1]
779 out_i
= valid_outputs
[0]
781 make_links
= [] # store sockets for new links
783 socket_type
= 'NodeSocketShader'
784 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
785 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
786 output_socket
= materialout
.inputs
[materialout_index
]
787 for li_from
, li_to
in make_links
:
788 connect_sockets(li_from
, li_to
)
790 # Create links through node groups until we reach the active node
791 tree
= base_node_tree
792 link_end
= output_socket
793 while tree
.nodes
.active
!= active
:
794 node
= tree
.nodes
.active
795 viewer_socket
= self
.ensure_viewer_socket(
796 node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
797 link_start
= node
.outputs
[viewer_socket_name
]
798 node_socket
= viewer_socket
799 if node_socket
in delete_sockets
:
800 delete_sockets
.remove(node_socket
)
801 connect_sockets(link_start
, link_end
)
803 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[viewer_socket_name
]
804 tree
= tree
.nodes
.active
.node_tree
805 connect_sockets(active
.outputs
[out_i
], link_end
)
808 for socket
in delete_sockets
:
809 if not self
.is_socket_used_other_mats(socket
):
810 tree
= socket
.id_data
811 self
.remove_socket(tree
, socket
)
813 nodes
.active
= active
816 force_update(context
)
823 class NWFrameSelected(Operator
, NWBase
):
824 bl_idname
= "node.nw_frame_selected"
825 bl_label
= "Frame Selected"
826 bl_description
= "Add a frame node and parent the selected nodes to it"
827 bl_options
= {'REGISTER', 'UNDO'}
829 label_prop
: StringProperty(
831 description
='The visual name of the frame node',
834 use_custom_color_prop
: BoolProperty(
836 description
="Use custom color for the frame node",
839 color_prop
: FloatVectorProperty(
841 description
="The color of the frame node",
842 default
=(0.604, 0.604, 0.604),
843 min=0, max=1, step
=1, precision
=3,
844 subtype
='COLOR_GAMMA', size
=3
847 def draw(self
, context
):
849 layout
.prop(self
, 'label_prop')
850 layout
.prop(self
, 'use_custom_color_prop')
851 col
= layout
.column()
852 col
.active
= self
.use_custom_color_prop
853 col
.prop(self
, 'color_prop', text
="")
855 def execute(self
, context
):
856 nodes
, links
= get_nodes_links(context
)
860 selected
.append(node
)
862 bpy
.ops
.node
.add_node(type='NodeFrame')
864 frm
.label
= self
.label_prop
865 frm
.use_custom_color
= self
.use_custom_color_prop
866 frm
.color
= self
.color_prop
868 for node
in selected
:
874 class NWReloadImages(Operator
):
875 bl_idname
= "node.nw_reload_images"
876 bl_label
= "Reload Images"
877 bl_description
= "Update all the image nodes to match their files on disk"
880 def poll(cls
, context
):
882 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
883 if context
.active_node
is not None:
884 for out
in context
.active_node
.outputs
:
885 if is_visible_socket(out
):
890 def execute(self
, context
):
891 nodes
, links
= get_nodes_links(context
)
892 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
895 if node
.type in image_types
:
896 if node
.type == "TEXTURE":
897 if node
.texture
: # node has texture assigned
898 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
899 if node
.texture
.image
: # texture has image assigned
900 node
.texture
.image
.reload()
908 self
.report({'INFO'}, "Reloaded images")
909 print("Reloaded " + str(num_reloaded
) + " images")
910 force_update(context
)
913 self
.report({'WARNING'}, "No images found to reload in this node tree")
917 class NWSwitchNodeType(Operator
, NWBase
):
918 """Switch type of selected nodes """
919 bl_idname
= "node.nw_swtch_node_type"
920 bl_label
= "Switch Node Type"
921 bl_options
= {'REGISTER', 'UNDO'}
923 to_type
: StringProperty(
924 name
="Switch to type",
928 def execute(self
, context
):
929 to_type
= self
.to_type
930 if len(to_type
) == 0:
933 nodes
, links
= get_nodes_links(context
)
934 # Those types of nodes will not swap.
935 src_excludes
= ('NodeFrame')
936 # Those attributes of nodes will be copied if possible
937 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
938 'show_options', 'show_preview', 'show_texture',
939 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
941 selected
= [n
for n
in nodes
if n
.select
]
943 for node
in [n
for n
in selected
if
944 n
.rna_type
.identifier
not in src_excludes
and
945 n
.rna_type
.identifier
!= to_type
]:
946 new_node
= nodes
.new(to_type
)
947 for attr
in attrs_to_pass
:
948 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
949 setattr(new_node
, attr
, getattr(node
, attr
))
950 # set image datablock of dst to image of src
951 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
953 new_node
.image
= node
.image
955 if new_node
.type == 'SWITCH':
957 # Dictionaries: src_sockets and dst_sockets:
958 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
959 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
960 # in 'INPUTS' and 'OUTPUTS':
961 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
963 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
965 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
966 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
969 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
970 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
972 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
973 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
974 # check src node to set src_sockets values and dst node to set dst_sockets dict values
975 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
976 # Check node's inputs and outputs and fill proper entries in "sockets" dict
977 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
978 # enumerate in inputs, then in outputs
979 # find name, default value and links of socket
980 for i
, socket
in enumerate(in_out
):
981 the_name
= socket
.name
983 # Not every socket, especially in outputs has "default_value"
984 if hasattr(socket
, 'default_value'):
985 dval
= socket
.default_value
987 for lnk
in socket
.links
:
988 socket_links
.append(lnk
)
989 # check type of socket to fill proper keys.
990 for the_type
in types_order_one
:
991 if socket
.type == the_type
:
992 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
993 # entry structure: (index_in_type, socket_index, socket_name,
994 # socket_default_value, socket_links)
995 sockets
[in_out_name
][the_type
].append(
996 (len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
997 # Check which of the types in inputs/outputs is considered to be "main".
998 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
999 for type_check
in types_order_one
:
1000 if sockets
[in_out_name
][type_check
]:
1001 sockets
[in_out_name
]['MAIN'] = type_check
1005 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1006 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1009 for inout
, soctype
in (
1010 ('INPUTS', 'MAIN',),
1011 ('INPUTS', 'SHADER',),
1012 ('INPUTS', 'RGBA',),
1013 ('INPUTS', 'VECTOR',),
1014 ('INPUTS', 'VALUE',),
1015 ('OUTPUTS', 'MAIN',),
1016 ('OUTPUTS', 'SHADER',),
1017 ('OUTPUTS', 'RGBA',),
1018 ('OUTPUTS', 'VECTOR',),
1019 ('OUTPUTS', 'VALUE',),
1021 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
1022 if soctype
== 'MAIN':
1023 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
1024 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
1026 sc
= src_sockets
[inout
][soctype
]
1027 dt
= dst_sockets
[inout
][soctype
]
1028 # start with 'dt' to determine number of possibilities.
1029 for i
, soc
in enumerate(dt
):
1030 # if src main has enough entries - match them with dst main sockets by indexes.
1032 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
1033 # add 'VALUE_NAME' criterion to inputs.
1034 if inout
== 'INPUTS' and soctype
== 'VALUE':
1036 if s
[2] == soc
[2]: # if names match
1037 # append src (index, dval), dst (index, dval)
1038 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
1040 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1041 # This creates better links when relinking textures.
1042 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
1043 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
1045 # Pass default values and RELINK:
1046 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1047 # INPUTS: Base on matches in proper order.
1048 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
1050 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
1051 new_node
.inputs
[dst_i
].default_value
= src_dval
1052 # Special case: switch to math
1053 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1054 new_node
.type == 'MATH' and\
1056 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
1057 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
1058 if node
.type == 'MIX_RGB':
1059 if node
.blend_type
in [o
[0] for o
in operations
]:
1060 new_node
.operation
= node
.blend_type
1061 # Special case: switch from math to some types
1062 if node
.type == 'MATH' and\
1063 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1066 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
1067 if new_node
.type == 'MIX_RGB':
1068 if node
.operation
in [t
[0] for t
in blend_types
]:
1069 new_node
.blend_type
= node
.operation
1070 # Set Fac of MIX_RGB to 1.0
1071 new_node
.inputs
[0].default_value
= 1.0
1072 # make link only when dst matching input is not linked already.
1073 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
1074 in_src_link
= node
.inputs
[src_i
].links
[0]
1075 in_dst_socket
= new_node
.inputs
[dst_i
]
1076 connect_sockets(in_src_link
.from_socket
, in_dst_socket
)
1077 links
.remove(in_src_link
)
1078 # OUTPUTS: Base on matches in proper order.
1079 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
1080 for out_src_link
in node
.outputs
[src_i
].links
:
1081 out_dst_socket
= new_node
.outputs
[dst_i
]
1082 connect_sockets(out_dst_socket
, out_src_link
.to_socket
)
1083 # relink rest inputs if possible, no criteria
1084 for src_inp
in node
.inputs
:
1085 for dst_inp
in new_node
.inputs
:
1086 if src_inp
.links
and not dst_inp
.links
:
1087 src_link
= src_inp
.links
[0]
1088 connect_sockets(src_link
.from_socket
, dst_inp
)
1089 links
.remove(src_link
)
1090 # relink rest outputs if possible, base on node kind if any left.
1091 for src_o
in node
.outputs
:
1092 for out_src_link
in src_o
.links
:
1093 for dst_o
in new_node
.outputs
:
1094 if src_o
.type == dst_o
.type:
1095 connect_sockets(dst_o
, out_src_link
.to_socket
)
1096 # relink rest outputs no criteria if any left. Link all from first output.
1097 for src_o
in node
.outputs
:
1098 for out_src_link
in src_o
.links
:
1099 if new_node
.outputs
:
1100 connect_sockets(new_node
.outputs
[0], out_src_link
.to_socket
)
1102 force_update(context
)
1106 class NWMergeNodes(Operator
, NWBase
):
1107 bl_idname
= "node.nw_merge_nodes"
1108 bl_label
= "Merge Nodes"
1109 bl_description
= "Merge Selected Nodes"
1110 bl_options
= {'REGISTER', 'UNDO'}
1114 description
="All possible blend types, boolean operations and math operations",
1115 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
],
1117 merge_type
: EnumProperty(
1119 description
="Type of Merge to be used",
1121 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1122 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1123 ('GEOMETRY', 'Geometry', 'Merge using Mesh Boolean or Join Geometry Node'),
1124 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1125 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1126 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
1127 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
1131 # Check if the link connects to a node that is in selected_nodes
1132 # If not, then check recursively for each link in the nodes outputs.
1133 # If yes, return True. If the recursion stops without finding a node
1134 # in selected_nodes, it returns False. The depth is used to prevent
1135 # getting stuck in a loop because of an already present cycle.
1137 def link_creates_cycle(link
, selected_nodes
, depth
=0) -> bool:
1139 # We're stuck in a cycle, but that cycle was already present,
1140 # so we return False.
1141 # NOTE: The number 255 is arbitrary, but seems to work well.
1144 if node
in selected_nodes
:
1146 if not node
.outputs
:
1148 for output
in node
.outputs
:
1149 if output
.is_linked
:
1150 for olink
in output
.links
:
1151 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+ 1):
1153 # None of the outputs found a node in selected_nodes, so there is no cycle.
1156 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
1157 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
1158 # be connected. The last one is assumed to be a multi input socket.
1159 # For convenience the node is returned.
1161 def merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
1162 # The y-location of the last node
1163 loc_y
= nodes_list
[-1][2]
1164 if merge_position
== 'CENTER':
1165 # Average the y-location
1166 for i
in range(len(nodes_list
) - 1):
1167 loc_y
+= nodes_list
[i
][2]
1168 loc_y
= loc_y
/ len(nodes_list
)
1169 new_node
= nodes
.new(node_name
)
1170 new_node
.hide
= do_hide
1171 new_node
.location
.x
= loc_x
1172 new_node
.location
.y
= loc_y
1173 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
1175 outputs_for_multi_input
= []
1176 for i
, node
in enumerate(selected_nodes
):
1178 # Search for the first node which had output links that do not create
1179 # a cycle, which we can then reconnect afterwards.
1180 if prev_links
== [] and node
.outputs
[0].is_linked
:
1182 link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(
1183 link
, selected_nodes
)]
1184 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
1185 # To get the placement to look right we need to reverse the order in which we connect the
1186 # outputs to the multi input socket.
1187 if i
< len(socket_indices
) - 1:
1188 ind
= socket_indices
[i
]
1189 connect_sockets(node
.outputs
[0], new_node
.inputs
[ind
])
1191 outputs_for_multi_input
.insert(0, node
.outputs
[0])
1192 if outputs_for_multi_input
!= []:
1193 ind
= socket_indices
[-1]
1194 for output
in outputs_for_multi_input
:
1195 connect_sockets(output
, new_node
.inputs
[ind
])
1196 if prev_links
!= []:
1197 for link
in prev_links
:
1198 connect_sockets(new_node
.outputs
[0], link
.to_node
.inputs
[0])
1201 def execute(self
, context
):
1202 settings
= context
.preferences
.addons
[__package__
].preferences
1203 merge_hide
= settings
.merge_hide
1204 merge_position
= settings
.merge_position
# 'center' or 'bottom'
1207 do_hide_shader
= False
1208 if merge_hide
== 'ALWAYS':
1210 do_hide_shader
= True
1211 elif merge_hide
== 'NON_SHADER':
1214 tree_type
= context
.space_data
.node_tree
.type
1215 if tree_type
== 'GEOMETRY':
1216 node_type
= 'GeometryNode'
1217 if tree_type
== 'COMPOSITING':
1218 node_type
= 'CompositorNode'
1219 elif tree_type
== 'SHADER':
1220 node_type
= 'ShaderNode'
1221 elif tree_type
== 'TEXTURE':
1222 node_type
= 'TextureNode'
1223 nodes
, links
= get_nodes_links(context
)
1225 merge_type
= self
.merge_type
1226 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
1227 # 'ZCOMBINE' works only if mode == 'MIX'
1228 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
1229 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
1232 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
1234 # The Mix node and math nodes used for geometry nodes are of type 'ShaderNode'
1235 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
1236 node_type
= 'ShaderNode'
1237 selected_mix
= [] # entry = [index, loc]
1238 selected_shader
= [] # entry = [index, loc]
1239 selected_geometry
= [] # entry = [index, loc]
1240 selected_math
= [] # entry = [index, loc]
1241 selected_vector
= [] # entry = [index, loc]
1242 selected_z
= [] # entry = [index, loc]
1243 selected_alphaover
= [] # entry = [index, loc]
1245 for i
, node
in enumerate(nodes
):
1246 if node
.select
and node
.outputs
:
1247 if merge_type
== 'AUTO':
1248 for (type, types_list
, dst
) in (
1249 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1250 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1251 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
1252 ('VALUE', [t
[0] for t
in operations
], selected_math
),
1253 ('VECTOR', [], selected_vector
),
1255 output
= get_first_enabled_output(node
)
1256 output_type
= output
.type
1257 valid_mode
= mode
in types_list
1258 # When mode is 'MIX' we have to cheat since the mix node is not used in
1260 if tree_type
== 'GEOMETRY':
1262 if output_type
== 'VALUE' and type == 'VALUE':
1264 elif output_type
== 'VECTOR' and type == 'VECTOR':
1266 elif type == 'GEOMETRY':
1268 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
1269 # Cheat that output type is 'RGBA',
1270 # and that 'MIX' exists in math operations list.
1271 # This way when selected_mix list is analyzed:
1272 # Node data will be appended even though it doesn't meet requirements.
1273 elif output_type
!= 'SHADER' and mode
== 'MIX':
1274 output_type
= 'RGBA'
1276 if output_type
== type and valid_mode
:
1277 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1279 for (type, types_list
, dst
) in (
1280 ('SHADER', ('MIX', 'ADD'), selected_shader
),
1281 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
1282 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
1283 ('MATH', [t
[0] for t
in operations
], selected_math
),
1284 ('ZCOMBINE', ('MIX', ), selected_z
),
1285 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
1287 if merge_type
== type and mode
in types_list
:
1288 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
1289 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
1290 # use only 'Mix' nodes for merging.
1291 # For that we add selected_math list to selected_mix list and clear selected_math.
1292 if selected_mix
and selected_math
and merge_type
== 'AUTO':
1293 selected_mix
+= selected_math
1302 selected_alphaover
]:
1305 count_before
= len(nodes
)
1306 # sort list by loc_x - reversed
1307 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
1309 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
1310 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
1312 # Change the node type for math nodes in a geometry node tree.
1313 if tree_type
== 'GEOMETRY':
1314 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
1315 node_type
= 'ShaderNode'
1319 node_type
= 'GeometryNode'
1320 if merge_position
== 'CENTER':
1321 # average yloc of last two nodes (lowest two)
1322 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2
1323 if nodes_list
[len(nodes_list
) - 1][-1]: # if last node is hidden, mix should be shifted up a bit
1329 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
1333 if nodes_list
== selected_shader
and not do_hide_shader
:
1335 the_range
= len(nodes_list
) - 1
1336 if len(nodes_list
) == 1:
1339 for i
in range(the_range
):
1340 if nodes_list
== selected_mix
:
1342 if tree_type
== 'COMPOSITING':
1344 add_type
= node_type
+ mix_name
1345 add
= nodes
.new(add_type
)
1346 if tree_type
!= 'COMPOSITING':
1347 add
.data_type
= 'RGBA'
1348 add
.blend_type
= mode
1350 add
.inputs
[0].default_value
= 1.0
1351 add
.show_preview
= False
1357 if tree_type
== 'COMPOSITING':
1360 elif nodes_list
== selected_math
:
1361 add_type
= node_type
+ 'Math'
1362 add
= nodes
.new(add_type
)
1363 add
.operation
= mode
1369 elif nodes_list
== selected_shader
:
1371 add_type
= node_type
+ 'MixShader'
1372 add
= nodes
.new(add_type
)
1373 add
.hide
= do_hide_shader
1379 add_type
= node_type
+ 'AddShader'
1380 add
= nodes
.new(add_type
)
1381 add
.hide
= do_hide_shader
1386 elif nodes_list
== selected_geometry
:
1387 if mode
in ('JOIN', 'MIX'):
1388 add_type
= node_type
+ 'JoinGeometry'
1389 add
= self
.merge_with_multi_input(
1390 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, [0])
1392 add_type
= node_type
+ 'MeshBoolean'
1393 indices
= [0, 1] if mode
== 'DIFFERENCE' else [1]
1394 add
= self
.merge_with_multi_input(
1395 nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
, indices
)
1396 add
.operation
= mode
1399 elif nodes_list
== selected_vector
:
1400 add_type
= node_type
+ 'VectorMath'
1401 add
= nodes
.new(add_type
)
1402 add
.operation
= mode
1408 elif nodes_list
== selected_z
:
1409 add
= nodes
.new('CompositorNodeZcombine')
1410 add
.show_preview
= False
1416 elif nodes_list
== selected_alphaover
:
1417 add
= nodes
.new('CompositorNodeAlphaOver')
1418 add
.show_preview
= False
1424 add
.location
= loc_x
, loc_y
1428 # This has already been handled separately
1432 count_after
= len(nodes
)
1433 index
= count_after
- 1
1434 first_selected
= nodes
[nodes_list
[0][0]]
1435 # "last" node has been added as first, so its index is count_before.
1436 last_add
= nodes
[count_before
]
1437 # Create list of invalid indexes.
1438 invalid_nodes
= [nodes
[n
[0]]
1439 for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
1442 # Two nodes were selected and first selected has no output links, second selected has output links.
1443 # Then add links from last add to all links 'to_socket' of out links of second selected.
1444 first_selected_output
= get_first_enabled_output(first_selected
)
1445 if len(nodes_list
) == 2:
1446 if not first_selected_output
.links
:
1447 second_selected
= nodes
[nodes_list
[1][0]]
1448 for ss_link
in get_first_enabled_output(second_selected
).links
:
1449 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
1450 # Link only if "to_node" index not in invalid indexes list.
1451 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
1452 connect_sockets(get_first_enabled_output(last_add
), ss_link
.to_socket
)
1453 # add links from last_add to all links 'to_socket' of out links of first selected.
1454 for fs_link
in first_selected_output
.links
:
1455 # Link only if "to_node" index not in invalid indexes list.
1456 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
1457 connect_sockets(get_first_enabled_output(last_add
), fs_link
.to_socket
)
1458 # add link from "first" selected and "first" add node
1459 node_to
= nodes
[count_after
- 1]
1460 connect_sockets(first_selected_output
, node_to
.inputs
[first
])
1461 if node_to
.type == 'ZCOMBINE':
1462 for fs_out
in first_selected
.outputs
:
1463 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
1464 connect_sockets(fs_out
, node_to
.inputs
[1])
1466 # add links between added ADD nodes and between selected and ADD nodes
1467 for i
in range(count_adds
):
1468 if i
< count_adds
- 1:
1469 node_from
= nodes
[index
]
1470 node_to
= nodes
[index
- 1]
1471 node_to_input_i
= first
1472 node_to_z_i
= 1 # if z combine - link z to first z input
1473 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1474 if node_to
.type == 'ZCOMBINE':
1475 for from_out
in node_from
.outputs
:
1476 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1477 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1478 if len(nodes_list
) > 1:
1479 node_from
= nodes
[nodes_list
[i
+ 1][0]]
1480 node_to
= nodes
[index
]
1481 node_to_input_i
= second
1482 node_to_z_i
= 3 # if z combine - link z to second z input
1483 connect_sockets(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
1484 if node_to
.type == 'ZCOMBINE':
1485 for from_out
in node_from
.outputs
:
1486 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
1487 connect_sockets(from_out
, node_to
.inputs
[node_to_z_i
])
1489 # set "last" of added nodes as active
1490 nodes
.active
= last_add
1491 for i
, x
, y
, dx
, h
in nodes_list
:
1492 nodes
[i
].select
= False
1497 class NWBatchChangeNodes(Operator
, NWBase
):
1498 bl_idname
= "node.nw_batch_change"
1499 bl_label
= "Batch Change"
1500 bl_description
= "Batch Change Blend Type and Math Operation"
1501 bl_options
= {'REGISTER', 'UNDO'}
1503 blend_type
: EnumProperty(
1505 items
=blend_types
+ navs
,
1507 operation
: EnumProperty(
1509 items
=operations
+ navs
,
1512 def execute(self
, context
):
1513 blend_type
= self
.blend_type
1514 operation
= self
.operation
1515 for node
in context
.selected_nodes
:
1516 if node
.type == 'MIX_RGB' or (node
.bl_idname
== 'ShaderNodeMix' and node
.data_type
== 'RGBA'):
1517 if blend_type
not in [nav
[0] for nav
in navs
]:
1518 node
.blend_type
= blend_type
1520 if blend_type
== 'NEXT':
1521 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1522 # index = blend_types.index(node.blend_type)
1523 if index
== len(blend_types
) - 1:
1524 node
.blend_type
= blend_types
[0][0]
1526 node
.blend_type
= blend_types
[index
+ 1][0]
1528 if blend_type
== 'PREV':
1529 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
1531 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
1533 node
.blend_type
= blend_types
[index
- 1][0]
1535 if node
.type == 'MATH' or node
.bl_idname
== 'ShaderNodeMath':
1536 if operation
not in [nav
[0] for nav
in navs
]:
1537 node
.operation
= operation
1539 if operation
== 'NEXT':
1540 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1541 # index = operations.index(node.operation)
1542 if index
== len(operations
) - 1:
1543 node
.operation
= operations
[0][0]
1545 node
.operation
= operations
[index
+ 1][0]
1547 if operation
== 'PREV':
1548 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
1549 # index = operations.index(node.operation)
1551 node
.operation
= operations
[len(operations
) - 1][0]
1553 node
.operation
= operations
[index
- 1][0]
1558 class NWChangeMixFactor(Operator
, NWBase
):
1559 bl_idname
= "node.nw_factor"
1560 bl_label
= "Change Factor"
1561 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
1562 bl_options
= {'REGISTER', 'UNDO'}
1564 # option: Change factor.
1565 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
1566 # Else - change factor by option value.
1567 option
: FloatProperty()
1569 def execute(self
, context
):
1570 nodes
, links
= get_nodes_links(context
)
1571 option
= self
.option
1572 selected
= [] # entry = index
1573 for si
, node
in enumerate(nodes
):
1575 if node
.type in {'MIX_RGB', 'MIX_SHADER'} or node
.bl_idname
== 'ShaderNodeMix':
1579 fac
= nodes
[si
].inputs
[0]
1580 nodes
[si
].hide
= False
1581 if option
in {0.0, 1.0}:
1582 fac
.default_value
= option
1584 fac
.default_value
+= option
1589 class NWCopySettings(Operator
, NWBase
):
1590 bl_idname
= "node.nw_copy_settings"
1591 bl_label
= "Copy Settings"
1592 bl_description
= "Copy Settings of Active Node to Selected Nodes"
1593 bl_options
= {'REGISTER', 'UNDO'}
1596 def poll(cls
, context
):
1598 if nw_check(context
):
1600 context
.active_node
is not None and
1601 context
.active_node
.type != 'FRAME'
1606 def execute(self
, context
):
1607 node_active
= context
.active_node
1608 node_selected
= context
.selected_nodes
1611 if not (len(node_selected
) > 1):
1612 self
.report({'ERROR'}, "2 nodes must be selected at least")
1613 return {'CANCELLED'}
1615 # Check if active node is in the selection
1616 selected_node_names
= [n
.name
for n
in node_selected
]
1617 if node_active
.name
not in selected_node_names
:
1618 self
.report({'ERROR'}, "No active node")
1619 return {'CANCELLED'}
1621 # Get nodes in selection by type
1622 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
1624 if not (len(valid_nodes
) > 1) and node_active
:
1625 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
1626 return {'CANCELLED'}
1628 if len(valid_nodes
) != len(node_selected
):
1629 # Report nodes that are not valid
1630 valid_node_names
= [n
.name
for n
in valid_nodes
]
1631 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
1634 "Ignored {} (not of the same type as {})".format(
1635 ", ".join(not_valid_names
),
1638 # Reference original
1640 # node_selected_names = [n.name for n in node_selected]
1645 # Deselect all nodes
1646 for i
in node_selected
:
1649 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
1650 # Run through all other nodes
1651 for node
in valid_nodes
[1:]:
1653 # Check for frame node
1654 parent
= node
.parent
if node
.parent
else None
1655 node_loc
= [node
.location
.x
, node
.location
.y
]
1657 # Select original to duplicate
1660 # Duplicate selected node
1661 bpy
.ops
.node
.duplicate()
1662 new_node
= context
.selected_nodes
[0]
1665 new_node
.select
= False
1667 # Properties to copy
1668 node_tree
= node
.id_data
1669 props_to_copy
= 'bl_idname name location height width'.split(' ')
1673 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
1674 for i
in (i
for i
in mappings
if i
.is_linked
):
1676 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
1679 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
1680 props_to_copy
.pop(0)
1682 for prop
in props_to_copy
:
1683 setattr(new_node
, prop
, props
[prop
])
1685 # Get the node tree to remove the old node
1686 nodes
= node_tree
.nodes
1688 new_node
.name
= props
['name']
1691 new_node
.parent
= parent
1692 new_node
.location
= node_loc
1694 for str_from
, str_to
in reconnections
:
1695 node_tree
.connect_sockets(eval(str_from
), eval(str_to
))
1697 success_names
.append(new_node
.name
)
1700 node_tree
.nodes
.active
= orig
1703 "Successfully copied attributes from {} to: {}".format(
1705 ", ".join(success_names
)))
1709 class NWCopyLabel(Operator
, NWBase
):
1710 bl_idname
= "node.nw_copy_label"
1711 bl_label
= "Copy Label"
1712 bl_options
= {'REGISTER', 'UNDO'}
1714 option
: EnumProperty(
1716 description
="Source of name of label",
1718 ('FROM_ACTIVE', 'from active', 'from active node',),
1719 ('FROM_NODE', 'from node', 'from node linked to selected node'),
1720 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
1724 def execute(self
, context
):
1725 nodes
, links
= get_nodes_links(context
)
1726 option
= self
.option
1727 active
= nodes
.active
1728 if option
== 'FROM_ACTIVE':
1730 src_label
= active
.label
1731 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
1732 node
.label
= src_label
1733 elif option
== 'FROM_NODE':
1734 selected
= [n
for n
in nodes
if n
.select
]
1735 for node
in selected
:
1736 for input in node
.inputs
:
1738 src
= input.links
[0].from_node
1739 node
.label
= src
.label
1741 elif option
== 'FROM_SOCKET':
1742 selected
= [n
for n
in nodes
if n
.select
]
1743 for node
in selected
:
1744 for input in node
.inputs
:
1746 src
= input.links
[0].from_socket
1747 node
.label
= src
.name
1753 class NWClearLabel(Operator
, NWBase
):
1754 bl_idname
= "node.nw_clear_label"
1755 bl_label
= "Clear Label"
1756 bl_options
= {'REGISTER', 'UNDO'}
1758 option
: BoolProperty()
1760 def execute(self
, context
):
1761 nodes
, links
= get_nodes_links(context
)
1762 for node
in [n
for n
in nodes
if n
.select
]:
1767 def invoke(self
, context
, event
):
1769 return self
.execute(context
)
1771 return context
.window_manager
.invoke_confirm(self
, event
)
1774 class NWModifyLabels(Operator
, NWBase
):
1775 """Modify Labels of all selected nodes"""
1776 bl_idname
= "node.nw_modify_labels"
1777 bl_label
= "Modify Labels"
1778 bl_options
= {'REGISTER', 'UNDO'}
1780 prepend
: StringProperty(
1781 name
="Add to Beginning"
1783 append
: StringProperty(
1786 replace_from
: StringProperty(
1787 name
="Text to Replace"
1789 replace_to
: StringProperty(
1793 def execute(self
, context
):
1794 nodes
, links
= get_nodes_links(context
)
1795 for node
in [n
for n
in nodes
if n
.select
]:
1796 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
1800 def invoke(self
, context
, event
):
1804 return context
.window_manager
.invoke_props_dialog(self
)
1807 class NWAddTextureSetup(Operator
, NWBase
):
1808 bl_idname
= "node.nw_add_texture"
1809 bl_label
= "Texture Setup"
1810 bl_description
= "Add Texture Node Setup to Selected Shaders"
1811 bl_options
= {'REGISTER', 'UNDO'}
1813 add_mapping
: BoolProperty(
1814 name
="Add Mapping Nodes",
1815 description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)",
1819 def poll(cls
, context
):
1820 if nw_check(context
):
1821 space
= context
.space_data
1822 if space
.tree_type
== 'ShaderNodeTree':
1826 def execute(self
, context
):
1827 nodes
, links
= get_nodes_links(context
)
1829 texture_types
= get_texture_node_types()
1830 selected_nodes
= [n
for n
in nodes
if n
.select
]
1832 for node
in selected_nodes
:
1837 target_input
= node
.inputs
[0]
1838 for input in node
.inputs
:
1841 if not input.is_linked
:
1842 target_input
= input
1845 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
1850 locx
= node
.location
.x
1851 locy
= node
.location
.y
- (input_index
* padding
)
1853 is_texture_node
= node
.rna_type
.identifier
in texture_types
1854 use_environment_texture
= node
.type == 'BACKGROUND'
1856 # Add an image texture before normal shader nodes.
1857 if not is_texture_node
:
1858 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
1859 image_texture_node
= nodes
.new(image_texture_type
)
1860 x_offset
= x_offset
+ image_texture_node
.width
+ padding
1861 image_texture_node
.location
= [locx
- x_offset
, locy
]
1862 nodes
.active
= image_texture_node
1863 connect_sockets(image_texture_node
.outputs
[0], target_input
)
1865 # The mapping setup following this will connect to the first input of this image texture.
1866 target_input
= image_texture_node
.inputs
[0]
1870 if is_texture_node
or self
.add_mapping
:
1872 mapping_node
= nodes
.new('ShaderNodeMapping')
1873 x_offset
= x_offset
+ mapping_node
.width
+ padding
1874 mapping_node
.location
= [locx
- x_offset
, locy
]
1875 connect_sockets(mapping_node
.outputs
[0], target_input
)
1877 # Add Texture Coordinates node.
1878 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
1879 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
1880 tex_coord_node
.location
= [locx
- x_offset
, locy
]
1882 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
1883 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
1884 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
1885 connect_sockets(tex_coord_output
, mapping_node
.inputs
[0])
1890 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
1891 bl_idname
= "node.nw_add_textures_for_principled"
1892 bl_label
= "Principled Texture Setup"
1893 bl_description
= "Add Texture Node Setup for Principled BSDF"
1894 bl_options
= {'REGISTER', 'UNDO'}
1896 directory
: StringProperty(
1900 description
='Folder to search in for image files'
1902 files
: CollectionProperty(
1903 type=bpy
.types
.OperatorFileListElement
,
1904 options
={'HIDDEN', 'SKIP_SAVE'}
1907 relative_path
: BoolProperty(
1908 name
='Relative Path',
1909 description
='Set the file path relative to the blend file, when possible',
1918 def draw(self
, context
):
1919 layout
= self
.layout
1920 layout
.alignment
= 'LEFT'
1922 layout
.prop(self
, 'relative_path')
1925 def poll(cls
, context
):
1927 if nw_check(context
):
1928 space
= context
.space_data
1929 if space
.tree_type
== 'ShaderNodeTree':
1933 def execute(self
, context
):
1934 # Check if everything is ok
1935 if not self
.directory
:
1936 self
.report({'INFO'}, 'No Folder Selected')
1937 return {'CANCELLED'}
1938 if not self
.files
[:]:
1939 self
.report({'INFO'}, 'No Files Selected')
1940 return {'CANCELLED'}
1942 nodes
, links
= get_nodes_links(context
)
1943 active_node
= nodes
.active
1944 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
1945 self
.report({'INFO'}, 'Select Principled BSDF')
1946 return {'CANCELLED'}
1948 # Filter textures names for texturetypes in filenames
1949 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
1950 tags
= context
.preferences
.addons
[__package__
].preferences
.principled_tags
1951 normal_abbr
= tags
.normal
.split(' ')
1952 bump_abbr
= tags
.bump
.split(' ')
1953 gloss_abbr
= tags
.gloss
.split(' ')
1954 rough_abbr
= tags
.rough
.split(' ')
1956 ['Displacement', tags
.displacement
.split(' '), None],
1957 ['Base Color', tags
.base_color
.split(' '), None],
1958 ['Metallic', tags
.metallic
.split(' '), None],
1959 ['Specular IOR Level', tags
.specular
.split(' '), None],
1960 ['Roughness', rough_abbr
+ gloss_abbr
, None],
1961 ['Normal', normal_abbr
+ bump_abbr
, None],
1962 ['Transmission Weight', tags
.transmission
.split(' '), None],
1963 ['Emission', tags
.emission
.split(' '), None],
1964 ['Alpha', tags
.alpha
.split(' '), None],
1965 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
1968 match_files_to_socket_names(self
.files
, socketnames
)
1969 # Remove socketnames without found files
1970 socketnames
= [s
for s
in socketnames
if s
[2]
1971 and path
.exists(self
.directory
+ s
[2])]
1973 self
.report({'INFO'}, 'No matching images found')
1974 print('No matching images found')
1975 return {'CANCELLED'}
1977 # Don't override path earlier as os.path is used to check the absolute path
1978 import_path
= self
.directory
1979 if self
.relative_path
:
1980 if bpy
.data
.filepath
:
1982 import_path
= bpy
.path
.relpath(self
.directory
)
1987 print('\nMatched Textures:')
1992 roughness_node
= None
1993 for i
, sname
in enumerate(socketnames
):
1994 print(i
, sname
[0], sname
[2])
1996 # DISPLACEMENT NODES
1997 if sname
[0] == 'Displacement':
1998 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
1999 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2000 disp_texture
.image
= img
2001 disp_texture
.label
= 'Displacement'
2002 if disp_texture
.image
:
2003 disp_texture
.image
.colorspace_settings
.is_data
= True
2005 # Add displacement offset nodes
2006 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
2007 # Align the Displacement node under the active Principled BSDF node
2008 disp_node
.location
= active_node
.location
+ Vector((100, -700))
2009 link
= connect_sockets(disp_node
.inputs
[0], disp_texture
.outputs
[0])
2011 # TODO Turn on true displacement in the material
2012 # Too complicated for now
2015 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
2017 if not output_node
[0].inputs
[2].is_linked
:
2018 link
= connect_sockets(output_node
[0].inputs
[2], disp_node
.outputs
[0])
2022 # AMBIENT OCCLUSION TEXTURE
2023 if sname
[0] == 'Ambient Occlusion':
2024 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
2025 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2026 ao_texture
.image
= img
2027 ao_texture
.label
= sname
[0]
2028 if ao_texture
.image
:
2029 ao_texture
.image
.colorspace_settings
.is_data
= True
2033 if not active_node
.inputs
[sname
[0]].is_linked
:
2034 # No texture node connected -> add texture node with new image
2035 texture_node
= nodes
.new(type='ShaderNodeTexImage')
2036 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2037 texture_node
.image
= img
2040 if sname
[0] == 'Normal':
2041 # Test if new texture node is normal or bump map
2042 fname_components
= split_into_components(sname
[2])
2043 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
2044 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
2046 # If Normal add normal node in between
2047 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
2048 link
= connect_sockets(normal_node
.inputs
[1], texture_node
.outputs
[0])
2050 # If Bump add bump node in between
2051 normal_node
= nodes
.new(type='ShaderNodeBump')
2052 link
= connect_sockets(normal_node
.inputs
[2], texture_node
.outputs
[0])
2054 link
= connect_sockets(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
2055 normal_node_texture
= texture_node
2057 elif sname
[0] == 'Roughness':
2058 # Test if glossy or roughness map
2059 fname_components
= split_into_components(sname
[2])
2060 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
2061 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
2064 # If Roughness nothing to to
2065 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2068 # If Gloss Map add invert node
2069 invert_node
= nodes
.new(type='ShaderNodeInvert')
2070 link
= connect_sockets(invert_node
.inputs
[1], texture_node
.outputs
[0])
2072 link
= connect_sockets(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
2073 roughness_node
= texture_node
2076 # This is a simple connection Texture --> Input slot
2077 link
= connect_sockets(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2079 # Use non-color for all but 'Base Color' Textures
2080 if not sname
[0] in ['Base Color', 'Emission'] and texture_node
.image
:
2081 texture_node
.image
.colorspace_settings
.is_data
= True
2084 # If already texture connected. add to node list for alignment
2085 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
2087 # This are all connected texture nodes
2088 texture_nodes
.append(texture_node
)
2089 texture_node
.label
= sname
[0]
2092 texture_nodes
.append(disp_texture
)
2095 # We want the ambient occlusion texture to be the top most texture node
2096 texture_nodes
.insert(0, ao_texture
)
2099 for i
, texture_node
in enumerate(texture_nodes
):
2100 offset
= Vector((-550, (i
* -280) + 200))
2101 texture_node
.location
= active_node
.location
+ offset
2104 # Extra alignment if normal node was added
2105 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
2108 # Alignment of invert node if glossy map
2109 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
2111 # Add texture input + mapping
2112 mapping
= nodes
.new(type='ShaderNodeMapping')
2113 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
2114 if len(texture_nodes
) > 1:
2115 # If more than one texture add reroute node in between
2116 reroute
= nodes
.new(type='NodeReroute')
2117 texture_nodes
.append(reroute
)
2118 tex_coords
= Vector((texture_nodes
[0].location
.x
,
2119 sum(n
.location
.y
for n
in texture_nodes
) / len(texture_nodes
)))
2120 reroute
.location
= tex_coords
+ Vector((-50, -120))
2121 for texture_node
in texture_nodes
:
2122 link
= connect_sockets(texture_node
.inputs
[0], reroute
.outputs
[0])
2123 link
= connect_sockets(reroute
.inputs
[0], mapping
.outputs
[0])
2125 link
= connect_sockets(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
2127 # Connect texture_coordiantes to mapping node
2128 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
2129 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
2130 link
= connect_sockets(mapping
.inputs
[0], texture_input
.outputs
[2])
2132 # Create frame around tex coords and mapping
2133 frame
= nodes
.new(type='NodeFrame')
2134 frame
.label
= 'Mapping'
2135 mapping
.parent
= frame
2136 texture_input
.parent
= frame
2139 # Create frame around texture nodes
2140 frame
= nodes
.new(type='NodeFrame')
2141 frame
.label
= 'Textures'
2142 for tnode
in texture_nodes
:
2143 tnode
.parent
= frame
2147 active_node
.select
= False
2150 force_update(context
)
2154 class NWAddReroutes(Operator
, NWBase
):
2155 """Add Reroute Nodes and link them to outputs of selected nodes"""
2156 bl_idname
= "node.nw_add_reroutes"
2157 bl_label
= "Add Reroutes"
2158 bl_description
= "Add Reroutes to Outputs"
2159 bl_options
= {'REGISTER', 'UNDO'}
2161 option
: EnumProperty(
2164 ('ALL', 'to all', 'Add to all outputs'),
2165 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2166 ('LINKED', 'to linked', 'Add only to linked outputs'),
2170 def execute(self
, context
):
2171 tree_type
= context
.space_data
.node_tree
.type
2172 option
= self
.option
2173 nodes
, links
= get_nodes_links(context
)
2174 # output valid when option is 'all' or when 'loose' output has no links
2176 post_select
= [] # nodes to be selected after execution
2177 # create reroutes and recreate links
2178 for node
in [n
for n
in nodes
if n
.select
]:
2183 # unhide 'REROUTE' nodes to avoid issues with location.y
2184 if node
.type == 'REROUTE':
2186 # Hack needed to calculate real width
2188 bpy
.ops
.node
.select_all(action
='DESELECT')
2189 helper
= nodes
.new('NodeReroute')
2190 helper
.select
= True
2192 # resize node and helper to zero. Then check locations to calculate width
2193 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2194 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2195 # restore node location
2196 node
.location
= x
, y
2199 # only helper is selected now
2200 bpy
.ops
.node
.delete()
2201 x
= node
.location
.x
+ width
+ 20.0
2202 if node
.type != 'REROUTE':
2206 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
2207 for out_i
, output
in enumerate(node
.outputs
):
2208 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
2209 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
2210 if node
.type != 'R_LAYERS':
2212 else: # if 'R_LAYERS' check if output represent used render pass
2213 node_scene
= node
.scene
2214 node_layer
= node
.layer
2215 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2216 if output
.name
== 'Alpha':
2219 # check entries in global 'rl_outputs' variable
2220 for rlo
in rl_outputs
:
2221 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2222 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
2225 valid
= ((option
== 'ALL') or
2226 (option
== 'LOOSE' and not output
.links
) or
2227 (option
== 'LINKED' and output
.links
))
2228 # Add reroutes only if valid, but offset location in all cases.
2230 n
= nodes
.new('NodeReroute')
2232 for link
in output
.links
:
2233 connect_sockets(n
.outputs
[0], link
.to_socket
)
2234 connect_sockets(output
, n
.inputs
[0])
2236 post_select
.append(n
)
2240 # disselect the node so that after execution of script only newly created nodes are selected
2242 # nicer reroutes distribution along y when node.hide
2244 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2245 for reroute
in [r
for r
in nodes
if r
.select
]:
2246 reroute
.location
.y
-= y_translate
2247 for node
in post_select
:
2253 class NWLinkActiveToSelected(Operator
, NWBase
):
2254 """Link active node to selected nodes basing on various criteria"""
2255 bl_idname
= "node.nw_link_active_to_selected"
2256 bl_label
= "Link Active Node to Selected"
2257 bl_options
= {'REGISTER', 'UNDO'}
2259 replace
: BoolProperty()
2260 use_node_name
: BoolProperty()
2261 use_outputs_names
: BoolProperty()
2264 def poll(cls
, context
):
2266 if nw_check(context
):
2267 if context
.active_node
is not None:
2268 if context
.active_node
.select
:
2272 def execute(self
, context
):
2273 nodes
, links
= get_nodes_links(context
)
2274 replace
= self
.replace
2275 use_node_name
= self
.use_node_name
2276 use_outputs_names
= self
.use_outputs_names
2277 active
= nodes
.active
2278 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
2279 outputs
= [] # Only usable outputs of active nodes will be stored here.
2280 for out
in active
.outputs
:
2281 if active
.type != 'R_LAYERS':
2284 # 'R_LAYERS' node type needs special handling.
2285 # outputs of 'R_LAYERS' are callable even if not seen in UI.
2286 # Only outputs that represent used passes should be taken into account
2287 # Check if pass represented by output is used.
2288 # global 'rl_outputs' list will be used for that
2289 for rlo
in rl_outputs
:
2290 pass_used
= False # initial value. Will be set to True if pass is used
2291 if out
.name
== 'Alpha':
2292 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
2294 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2295 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
2296 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
2300 doit
= True # Will be changed to False when links successfully added to previous output.
2303 for node
in selected
:
2304 dst_name
= node
.name
# Will be compared with src_name if needed.
2305 # When node has label - use it as dst_name
2307 dst_name
= node
.label
2308 valid
= True # Initial value. Will be changed to False if names don't match.
2309 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
2311 # Set src_name to source node name or label
2312 src_name
= active
.name
2314 src_name
= active
.label
2315 elif use_outputs_names
:
2316 src_name
= (out
.name
, )
2317 for rlo
in rl_outputs
:
2318 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2319 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
2320 if dst_name
not in src_name
:
2323 for input in node
.inputs
:
2324 if input.type == out
.type or node
.type == 'REROUTE':
2325 if replace
or not input.is_linked
:
2326 connect_sockets(out
, input)
2327 if not use_node_name
and not use_outputs_names
:
2334 class NWAlignNodes(Operator
, NWBase
):
2335 '''Align the selected nodes neatly in a row/column'''
2336 bl_idname
= "node.nw_align_nodes"
2337 bl_label
= "Align Nodes"
2338 bl_options
= {'REGISTER', 'UNDO'}
2339 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
2341 def execute(self
, context
):
2342 nodes
, links
= get_nodes_links(context
)
2343 margin
= self
.margin
2347 if node
.select
and node
.type != 'FRAME':
2348 selection
.append(node
)
2350 # If no nodes are selected, align all nodes
2354 elif nodes
.active
in selection
:
2355 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
2357 # Check if nodes should be laid out horizontally or vertically
2358 # use dimension to get center of node, not corner
2359 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2360 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
2361 x_range
= max(x_locs
) - min(x_locs
)
2362 y_range
= max(y_locs
) - min(y_locs
)
2363 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
2364 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
2365 horizontal
= x_range
> y_range
2367 # Sort selection by location of node mid-point
2369 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
2371 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
2375 for node
in selection
:
2376 current_margin
= margin
2377 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
2380 node
.location
.x
= current_pos
2381 current_pos
+= current_margin
+ node
.dimensions
.x
2382 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
2384 node
.location
.y
= current_pos
2385 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
2386 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
2388 # If active node is selected, center nodes around it
2389 if active_loc
is not None:
2390 active_loc_diff
= active_loc
- nodes
.active
.location
2391 for node
in selection
:
2392 node
.location
+= active_loc_diff
2393 else: # Position nodes centered around where they used to be
2394 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]
2395 ) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
2396 new_mid
= (max(locs
) + min(locs
)) / 2
2397 for node
in selection
:
2399 node
.location
.x
+= (mid_x
- new_mid
)
2401 node
.location
.y
+= (mid_y
- new_mid
)
2406 class NWSelectParentChildren(Operator
, NWBase
):
2407 bl_idname
= "node.nw_select_parent_child"
2408 bl_label
= "Select Parent or Children"
2409 bl_options
= {'REGISTER', 'UNDO'}
2411 option
: EnumProperty(
2414 ('PARENT', 'Select Parent', 'Select Parent Frame'),
2415 ('CHILD', 'Select Children', 'Select members of selected frame'),
2419 def execute(self
, context
):
2420 nodes
, links
= get_nodes_links(context
)
2421 option
= self
.option
2422 selected
= [node
for node
in nodes
if node
.select
]
2423 if option
== 'PARENT':
2424 for sel
in selected
:
2427 parent
.select
= True
2428 else: # option == 'CHILD'
2429 for sel
in selected
:
2430 children
= [node
for node
in nodes
if node
.parent
== sel
]
2431 for kid
in children
:
2437 class NWDetachOutputs(Operator
, NWBase
):
2438 """Detach outputs of selected node leaving inputs linked"""
2439 bl_idname
= "node.nw_detach_outputs"
2440 bl_label
= "Detach Outputs"
2441 bl_options
= {'REGISTER', 'UNDO'}
2443 def execute(self
, context
):
2444 nodes
, links
= get_nodes_links(context
)
2445 selected
= context
.selected_nodes
2446 bpy
.ops
.node
.duplicate_move_keep_inputs()
2447 new_nodes
= context
.selected_nodes
2448 bpy
.ops
.node
.select_all(action
="DESELECT")
2449 for node
in selected
:
2451 bpy
.ops
.node
.delete_reconnect()
2452 for new_node
in new_nodes
:
2453 new_node
.select
= True
2454 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
2459 class NWLinkToOutputNode(Operator
):
2460 """Link to Composite node or Material Output node"""
2461 bl_idname
= "node.nw_link_out"
2462 bl_label
= "Connect to Output"
2463 bl_options
= {'REGISTER', 'UNDO'}
2466 def poll(cls
, context
):
2468 if nw_check(context
):
2469 if context
.active_node
is not None:
2470 for out
in context
.active_node
.outputs
:
2471 if is_visible_socket(out
):
2476 def execute(self
, context
):
2477 nodes
, links
= get_nodes_links(context
)
2478 active
= nodes
.active
2480 tree_type
= context
.space_data
.tree_type
2481 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
2482 'WORLD': 'ShaderNodeOutputWorld',
2483 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
2485 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
2486 'CompositorNodeTree': 'CompositorNodeComposite',
2487 'TextureNodeTree': 'TextureNodeOutput',
2488 'GeometryNodeTree': 'NodeGroupOutput',
2491 # check whether the node is an output node and,
2492 # if supported, whether it's the active one
2493 if node
.rna_type
.identifier
== output_type \
2494 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
2498 else: # No output node exists
2499 bpy
.ops
.node
.select_all(action
="DESELECT")
2500 output_node
= nodes
.new(output_type
)
2501 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
2502 output_node
.location
.y
= active
.location
.y
2505 for i
, output
in enumerate(active
.outputs
):
2506 if is_visible_socket(output
):
2509 for i
, output
in enumerate(active
.outputs
):
2510 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
2515 if tree_type
== 'ShaderNodeTree':
2516 if active
.outputs
[output_index
].name
== 'Volume':
2518 elif active
.outputs
[output_index
].name
== 'Displacement':
2520 elif tree_type
== 'GeometryNodeTree':
2521 if active
.outputs
[output_index
].type != 'GEOMETRY':
2522 return {'CANCELLED'}
2523 connect_sockets(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
2525 force_update(context
) # viewport render does not update
2530 class NWMakeLink(Operator
, NWBase
):
2531 """Make a link from one socket to another"""
2532 bl_idname
= 'node.nw_make_link'
2533 bl_label
= 'Make Link'
2534 bl_options
= {'REGISTER', 'UNDO'}
2535 from_socket
: IntProperty()
2536 to_socket
: IntProperty()
2538 def execute(self
, context
):
2539 nodes
, links
= get_nodes_links(context
)
2541 n1
= nodes
[context
.scene
.NWLazySource
]
2542 n2
= nodes
[context
.scene
.NWLazyTarget
]
2544 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
2546 force_update(context
)
2551 class NWCallInputsMenu(Operator
, NWBase
):
2552 """Link from this output"""
2553 bl_idname
= 'node.nw_call_inputs_menu'
2554 bl_label
= 'Make Link'
2555 bl_options
= {'REGISTER', 'UNDO'}
2556 from_socket
: IntProperty()
2558 def execute(self
, context
):
2559 nodes
, links
= get_nodes_links(context
)
2561 context
.scene
.NWSourceSocket
= self
.from_socket
2563 n1
= nodes
[context
.scene
.NWLazySource
]
2564 n2
= nodes
[context
.scene
.NWLazyTarget
]
2565 if len(n2
.inputs
) > 1:
2566 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
2567 elif len(n2
.inputs
) == 1:
2568 connect_sockets(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
2572 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
2573 """Add an Image Sequence"""
2574 bl_idname
= 'node.nw_add_sequence'
2575 bl_label
= 'Import Image Sequence'
2576 bl_options
= {'REGISTER', 'UNDO'}
2578 directory
: StringProperty(
2581 filename
: StringProperty(
2584 files
: CollectionProperty(
2585 type=bpy
.types
.OperatorFileListElement
,
2586 options
={'HIDDEN', 'SKIP_SAVE'}
2588 relative_path
: BoolProperty(
2589 name
='Relative Path',
2590 description
='Set the file path relative to the blend file, when possible',
2594 def draw(self
, context
):
2595 layout
= self
.layout
2596 layout
.alignment
= 'LEFT'
2598 layout
.prop(self
, 'relative_path')
2600 def execute(self
, context
):
2601 nodes
, links
= get_nodes_links(context
)
2602 directory
= self
.directory
2603 filename
= self
.filename
2605 tree
= context
.space_data
.node_tree
2608 # print ("\nDIR:", directory)
2609 # print ("FN:", filename)
2610 # print ("Fs:", list(f.name for f in files), '\n')
2612 if tree
.type == 'SHADER':
2613 node_type
= "ShaderNodeTexImage"
2614 elif tree
.type == 'COMPOSITING':
2615 node_type
= "CompositorNodeImage"
2617 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2618 return {'CANCELLED'}
2620 if not files
[0].name
and not filename
:
2621 self
.report({'ERROR'}, "No file chosen")
2622 return {'CANCELLED'}
2623 elif files
[0].name
and (not filename
or not path
.exists(directory
+ filename
)):
2624 # User has selected multiple files without an active one, or the active one is non-existent
2625 filename
= files
[0].name
2627 if not path
.exists(directory
+ filename
):
2628 self
.report({'ERROR'}, filename
+ " does not exist!")
2629 return {'CANCELLED'}
2631 without_ext
= '.'.join(filename
.split('.')[:-1])
2633 # if last digit isn't a number, it's not a sequence
2634 if not without_ext
[-1].isdigit():
2635 self
.report({'ERROR'}, filename
+ " does not seem to be part of a sequence")
2636 return {'CANCELLED'}
2638 extension
= filename
.split('.')[-1]
2639 reverse
= without_ext
[::-1] # reverse string
2642 for char
in reverse
:
2648 without_num
= without_ext
[:count_numbers
* -1]
2650 files
= sorted(glob(directory
+ without_num
+ "[0-9]" * count_numbers
+ "." + extension
))
2652 num_frames
= len(files
)
2654 nodes_list
= [node
for node
in nodes
]
2656 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
2657 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
2661 yloc
+= node_mid_pt(node
, 'y')
2662 yloc
= yloc
/ len(nodes
)
2667 name_with_hashes
= without_num
+ "#" * count_numbers
+ '.' + extension
2669 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
2671 node
.label
= name_with_hashes
2673 filepath
= directory
+ (without_ext
+ '.' + extension
)
2674 if self
.relative_path
:
2675 if bpy
.data
.filepath
:
2677 filepath
= bpy
.path
.relpath(filepath
)
2681 img
= bpy
.data
.images
.load(filepath
)
2682 img
.source
= 'SEQUENCE'
2683 img
.name
= name_with_hashes
2685 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
2686 # separate the number from the file name of the first file
2687 image_user
.frame_offset
= int(files
[0][len(without_num
) + len(directory
):-1 * (len(extension
) + 1)]) - 1
2688 image_user
.frame_duration
= num_frames
2693 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
2694 """Add multiple images at once"""
2695 bl_idname
= 'node.nw_add_multiple_images'
2696 bl_label
= 'Open Selected Images'
2697 bl_options
= {'REGISTER', 'UNDO'}
2698 directory
: StringProperty(
2701 files
: CollectionProperty(
2702 type=bpy
.types
.OperatorFileListElement
,
2703 options
={'HIDDEN', 'SKIP_SAVE'}
2706 def execute(self
, context
):
2707 nodes
, links
= get_nodes_links(context
)
2709 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/ 2, context
.area
.height
/ 2)
2711 if context
.space_data
.node_tree
.type == 'SHADER':
2712 node_type
= "ShaderNodeTexImage"
2713 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
2714 node_type
= "CompositorNodeImage"
2716 self
.report({'ERROR'}, "Unsupported Node Tree type!")
2717 return {'CANCELLED'}
2720 for f
in self
.files
:
2723 node
= nodes
.new(node_type
)
2724 new_nodes
.append(node
)
2727 node
.location
.x
= xloc
2728 node
.location
.y
= yloc
2731 img
= bpy
.data
.images
.load(self
.directory
+ fname
)
2734 # shift new nodes up to center of tree
2735 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
2737 if node
in new_nodes
:
2739 node
.location
.y
+= (list_size
/ 2)
2745 class NWViewerFocus(bpy
.types
.Operator
):
2746 """Set the viewer tile center to the mouse position"""
2747 bl_idname
= "node.nw_viewer_focus"
2748 bl_label
= "Viewer Focus"
2750 x
: bpy
.props
.IntProperty()
2751 y
: bpy
.props
.IntProperty()
2754 def poll(cls
, context
):
2755 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
2757 def execute(self
, context
):
2760 def invoke(self
, context
, event
):
2761 render
= context
.scene
.render
2762 space
= context
.space_data
2763 percent
= render
.resolution_percentage
* 0.01
2765 nodes
, links
= get_nodes_links(context
)
2766 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
2769 mlocx
= event
.mouse_region_x
2770 mlocy
= event
.mouse_region_y
2771 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
2773 if 'FINISHED' not in select_node
: # only run if we're not clicking on a node
2774 region_x
= context
.region
.width
2775 region_y
= context
.region
.height
2777 region_center_x
= context
.region
.width
/ 2
2778 region_center_y
= context
.region
.height
/ 2
2780 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
2781 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
2783 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
2784 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
2786 margin_x
= region_center_x
- backdrop_center_x
2787 margin_y
= region_center_y
- backdrop_center_y
2789 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
2790 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
2792 for node
in viewers
:
2793 node
.center_x
= abs_mouse_x
2794 node
.center_y
= abs_mouse_y
2796 return {'PASS_THROUGH'}
2798 return self
.execute(context
)
2801 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
2802 """Save the current viewer node to an image file"""
2803 bl_idname
= "node.nw_save_viewer"
2804 bl_label
= "Save This Image"
2805 filepath
: StringProperty(subtype
="FILE_PATH")
2806 filename_ext
: EnumProperty(
2808 description
="Choose the file format to save to",
2809 items
=(('.bmp', "BMP", ""),
2810 ('.rgb', 'IRIS', ""),
2811 ('.png', 'PNG', ""),
2812 ('.jpg', 'JPEG', ""),
2813 ('.jp2', 'JPEG2000', ""),
2814 ('.tga', 'TARGA', ""),
2815 ('.cin', 'CINEON', ""),
2816 ('.dpx', 'DPX', ""),
2817 ('.exr', 'OPEN_EXR', ""),
2818 ('.hdr', 'HDR', ""),
2819 ('.tif', 'TIFF', "")),
2824 def poll(cls
, context
):
2826 if nw_check(context
):
2827 if context
.space_data
.tree_type
== 'CompositorNodeTree':
2828 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
2829 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
2833 def execute(self
, context
):
2850 basename
, ext
= path
.splitext(fp
)
2851 old_render_format
= context
.scene
.render
.image_settings
.file_format
2852 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
2853 context
.area
.type = "IMAGE_EDITOR"
2854 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
2855 context
.area
.spaces
[0].image
.save_render(fp
)
2856 context
.area
.type = "NODE_EDITOR"
2857 context
.scene
.render
.image_settings
.file_format
= old_render_format
2861 class NWResetNodes(bpy
.types
.Operator
):
2862 """Reset Nodes in Selection"""
2863 bl_idname
= "node.nw_reset_nodes"
2864 bl_label
= "Reset Nodes"
2865 bl_options
= {'REGISTER', 'UNDO'}
2868 def poll(cls
, context
):
2869 space
= context
.space_data
2870 return space
.type == 'NODE_EDITOR'
2872 def execute(self
, context
):
2873 node_active
= context
.active_node
2874 node_selected
= context
.selected_nodes
2875 node_ignore
= ["FRAME", "REROUTE", "GROUP", "SIMULATION_INPUT", "SIMULATION_OUTPUT"]
2877 # Check if one node is selected at least
2878 if not (len(node_selected
) > 0):
2879 self
.report({'ERROR'}, "1 node must be selected at least")
2880 return {'CANCELLED'}
2882 active_node_name
= node_active
.name
if node_active
.select
else None
2883 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
2885 # Create output lists
2886 selected_node_names
= [n
.name
for n
in node_selected
]
2889 # Reset all valid children in a frame
2890 node_active_is_frame
= False
2891 if len(node_selected
) == 1 and node_active
.type == "FRAME":
2892 node_tree
= node_active
.id_data
2893 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
2895 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
2896 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
2897 node_active_is_frame
= True
2899 # Check if valid nodes in selection
2900 if not (len(valid_nodes
) > 0):
2901 # Check for frames only
2902 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
2903 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
2904 self
.report({'ERROR'}, "Please select only 1 frame to reset")
2906 self
.report({'ERROR'}, "No valid node(s) in selection")
2907 return {'CANCELLED'}
2909 # Report nodes that are not valid
2910 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
2911 valid_node_names
= [n
.name
for n
in valid_nodes
]
2912 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2913 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
2915 # Deselect all nodes
2916 for i
in node_selected
:
2919 # Run through all valid nodes
2920 for node
in valid_nodes
:
2922 parent
= node
.parent
if node
.parent
else None
2923 node_loc
= [node
.location
.x
, node
.location
.y
]
2925 node_tree
= node
.id_data
2926 props_to_copy
= 'bl_idname name location height width'.split(' ')
2929 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2930 for i
in (i
for i
in mappings
if i
.is_linked
):
2932 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2934 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2936 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
2937 props_to_copy
.pop(0)
2939 for prop
in props_to_copy
:
2940 setattr(new_node
, prop
, props
[prop
])
2942 nodes
= node_tree
.nodes
2944 new_node
.name
= props
['name']
2947 new_node
.parent
= parent
2948 new_node
.location
= node_loc
2950 for str_from
, str_to
in reconnections
:
2951 connect_sockets(eval(str_from
), eval(str_to
))
2953 new_node
.select
= False
2954 success_names
.append(new_node
.name
)
2956 # Reselect all nodes
2957 if selected_node_names
and node_active_is_frame
is False:
2958 for i
in selected_node_names
:
2959 node_tree
.nodes
[i
].select
= True
2961 if active_node_name
is not None:
2962 node_tree
.nodes
[active_node_name
].select
= True
2963 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
2965 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
2988 NWAddPrincipledSetup
,
2990 NWLinkActiveToSelected
,
2992 NWSelectParentChildren
,
2998 NWAddMultipleImages
,
3006 from bpy
.utils
import register_class
3012 from bpy
.utils
import unregister_class
3015 unregister_class(cls
)