# ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### bl_info = { 'name': "Nodes Efficiency Tools", 'author': "Bartek Skorupa", 'version': (2, 31), 'blender': (2, 6, 7), 'location': "Node Editor Properties Panel (Ctrl-SPACE)", 'description': "Nodes Efficiency Tools", 'warning': "", 'wiki_url': "http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Nodes/Nodes_Efficiency_Tools", 'tracker_url': "http://projects.blender.org/tracker/index.php?func=detail&aid=33543&group_id=153&atid=469", 'category': "Node", } import bpy from bpy.types import Operator, Panel, Menu from bpy.props import FloatProperty, EnumProperty, BoolProperty from mathutils import Vector ################# # rl_outputs: # list of outputs of Input Render Layer # with attributes determinig if pass is used, # and MultiLayer EXR outputs names and corresponding render engines # # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles) rl_outputs = ( ('use_pass_ambient_occlusion', 'AO', 'AO', True, True), ('use_pass_color', 'Color', 'Color', True, False), ('use_pass_combined', 'Image', 'Combined', True, True), ('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False), ('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True), ('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True), ('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True), ('use_pass_emit', 'Emit', 'Emit', True, False), ('use_pass_environment', 'Environment', 'Env', True, False), ('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True), ('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True), ('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True), ('use_pass_indirect', 'Indirect', 'Indirect', True, False), ('use_pass_material_index', 'IndexMA', 'IndexMA', True, True), ('use_pass_mist', 'Mist', 'Mist', True, False), ('use_pass_normal', 'Normal', 'Normal', True, True), ('use_pass_object_index', 'IndexOB', 'IndexOB', True, True), ('use_pass_reflection', 'Reflect', 'Reflect', True, False), ('use_pass_refraction', 'Refract', 'Refract', True, False), ('use_pass_shadow', 'Shadow', 'Shadow', True, True), ('use_pass_specular', 'Specular', 'Spec', True, False), ('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True), ('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True), ('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True), ('use_pass_uv', 'UV', 'UV', True, True), ('use_pass_vector', 'Speed', 'Vector', True, True), ('use_pass_z', 'Z', 'Depth', True, True), ) # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty. blend_types = [ ('MIX', 'Mix', 'Mix Mode'), ('ADD', 'Add', 'Add Mode'), ('MULTIPLY', 'Multiply', 'Multiply Mode'), ('SUBTRACT', 'Subtract', 'Subtract Mode'), ('SCREEN', 'Screen', 'Screen Mode'), ('DIVIDE', 'Divide', 'Divide Mode'), ('DIFFERENCE', 'Difference', 'Difference Mode'), ('DARKEN', 'Darken', 'Darken Mode'), ('LIGHTEN', 'Lighten', 'Lighten Mode'), ('OVERLAY', 'Overlay', 'Overlay Mode'), ('DODGE', 'Dodge', 'Dodge Mode'), ('BURN', 'Burn', 'Burn Mode'), ('HUE', 'Hue', 'Hue Mode'), ('SATURATION', 'Saturation', 'Saturation Mode'), ('VALUE', 'Value', 'Value Mode'), ('COLOR', 'Color', 'Color Mode'), ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'), ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'), ] # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty. operations = [ ('ADD', 'Add', 'Add Mode'), ('MULTIPLY', 'Multiply', 'Multiply Mode'), ('SUBTRACT', 'Subtract', 'Subtract Mode'), ('DIVIDE', 'Divide', 'Divide Mode'), ('SINE', 'Sine', 'Sine Mode'), ('COSINE', 'Cosine', 'Cosine Mode'), ('TANGENT', 'Tangent', 'Tangent Mode'), ('ARCSINE', 'Arcsine', 'Arcsine Mode'), ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'), ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'), ('POWER', 'Power', 'Power Mode'), ('LOGARITHM', 'Logatithm', 'Logarithm Mode'), ('MINIMUM', 'Minimum', 'Minimum Mode'), ('MAXIMUM', 'Maximum', 'Maximum Mode'), ('ROUND', 'Round', 'Round Mode'), ('LESS_THAN', 'Less Than', 'Less Thann Mode'), ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'), ] # in BatchChangeNodes additional types/operations in a form that can be used as 'items' for EnumProperty. navs = [ ('CURRENT', 'Current', 'Leave at current state'), ('NEXT', 'Next', 'Next blend type/operation'), ('PREV', 'Prev', 'Previous blend type/operation'), ] # list of mixing shaders merge_shaders_types = ('MIX', 'ADD') # list of regular shaders. Entry: (identified, type, name for humans). Will be used in SwapShaders and menus. # Keeping mixed case to avoid having to translate entries when adding new nodes in SwapNodes. regular_shaders = ( ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'), ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'), ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'), ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'), ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'), ('ShaderNodeEmission', 'EMISSION', 'Emission'), ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'), ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'), ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'), ('ShaderNodeBackground', 'BACKGROUND', 'Background'), ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'), ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'), ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'), ) merge_shaders = ( ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'), ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'), ) def get_nodes_links(context): space = context.space_data tree = space.node_tree nodes = tree.nodes links = tree.links active = nodes.active context_active = context.active_node # check if we are working on regular node tree or node group is currently edited. # if group is edited - active node of space_tree is the group # if context.active_node != space active node - it means that the group is being edited. # in such case we set "nodes" to be nodes of this group, "links" to be links of this group # if context.active_node == space.active_node it means that we are not currently editing group is_main_tree = True if active: is_main_tree = context_active == active if not is_main_tree: # if group is currently edited tree = active.node_tree nodes = tree.nodes links = tree.links return nodes, links class NodeToolBase: @classmethod def poll(cls, context): space = context.space_data return space.type == 'NODE_EDITOR' and space.node_tree is not None class MergeNodes(Operator, NodeToolBase): bl_idname = "node.merge_nodes" bl_label = "Merge Nodes" bl_description = "Merge Selected Nodes" bl_options = {'REGISTER', 'UNDO'} mode = EnumProperty( name="mode", description="All possible blend types and math operations", items=blend_types + [op for op in operations if op not in blend_types], ) merge_type = EnumProperty( name="merge type", description="Type of Merge to be used", items=( ('AUTO', 'Auto', 'Automatic Output Type Detection'), ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'), ('MIX', 'Mix Node', 'Merge using Mix Nodes'), ('MATH', 'Math Node', 'Merge using Math Nodes'), ), ) def execute(self, context): tree_type = context.space_data.node_tree.type if tree_type == 'COMPOSITING': node_type = 'CompositorNode' elif tree_type == 'SHADER': node_type = 'ShaderNode' nodes, links = get_nodes_links(context) mode = self.mode merge_type = self.merge_type selected_mix = [] # entry = [index, loc] selected_shader = [] # entry = [index, loc] selected_math = [] # entry = [index, loc] for i, node in enumerate(nodes): if node.select and node.outputs: if merge_type == 'AUTO': for (type, types_list, dst) in ( ('SHADER', merge_shaders_types, selected_shader), ('RGBA', [t[0] for t in blend_types], selected_mix), ('VALUE', [t[0] for t in operations], selected_math), ): output_type = node.outputs[0].type valid_mode = mode in types_list # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types. # Cheat that output type is 'RGBA', # and that 'MIX' exists in math operations list. # This way when selected_mix list is analyzed: # Node data will be appended even though it doesn't meet requirements. if output_type != 'SHADER' and mode == 'MIX': output_type = 'RGBA' valid_mode = True if output_type == type and valid_mode: dst.append([i, node.location.x, node.location.y]) else: for (type, types_list, dst) in ( ('SHADER', merge_shaders_types, selected_shader), ('MIX', [t[0] for t in blend_types], selected_mix), ('MATH', [t[0] for t in operations], selected_math), ): if merge_type == type and mode in types_list: dst.append([i, node.location.x, node.location.y]) # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time # use only 'Mix' nodes for merging. # For that we add selected_math list to selected_mix list and clear selected_math. if selected_mix and selected_math and merge_type == 'AUTO': selected_mix += selected_math selected_math = [] for nodes_list in [selected_mix, selected_shader, selected_math]: if nodes_list: count_before = len(nodes) # sort list by loc_x - reversed nodes_list.sort(key=lambda k: k[1], reverse=True) # get maximum loc_x loc_x = nodes_list[0][1] + 350.0 nodes_list.sort(key=lambda k: k[2], reverse=True) loc_y = nodes_list[len(nodes_list) - 1][2] offset_y = 40.0 if nodes_list == selected_shader: offset_y = 150.0 the_range = len(nodes_list) - 1 do_hide = True if len(nodes_list) == 1: the_range = 1 do_hide = False for i in range(the_range): if nodes_list == selected_mix: add_type = node_type + 'MixRGB' add = nodes.new(add_type) add.blend_type = mode add.show_preview = False add.hide = do_hide first = 1 second = 2 add.width_hidden = 100.0 elif nodes_list == selected_math: add_type = node_type + 'Math' add = nodes.new(add_type) add.operation = mode add.hide = do_hide first = 0 second = 1 add.width_hidden = 100.0 elif nodes_list == selected_shader: if mode == 'MIX': add_type = node_type + 'MixShader' add = nodes.new(add_type) first = 1 second = 2 add.width_hidden = 100.0 elif mode == 'ADD': add_type = node_type + 'AddShader' add = nodes.new(add_type) first = 0 second = 1 add.width_hidden = 100.0 add.location = loc_x, loc_y loc_y += offset_y add.select = True count_adds = i + 1 count_after = len(nodes) index = count_after - 1 first_selected = nodes[nodes_list[0][0]] # "last" node has been added as first, so its index is count_before. last_add = nodes[count_before] # add links from last_add to all links 'to_socket' of out links of first selected. for fs_link in first_selected.outputs[0].links: # Prevent cyclic dependencies when nodes to be marged are linked to one another. # Create list of invalid indexes. invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader)] # Link only if "to_node" index not in invalid indexes list. if fs_link.to_node not in [nodes[i] for i in invalid_i]: links.new(last_add.outputs[0], fs_link.to_socket) # add link from "first" selected and "first" add node links.new(first_selected.outputs[0], nodes[count_after - 1].inputs[first]) # add links between added ADD nodes and between selected and ADD nodes for i in range(count_adds): if i < count_adds - 1: links.new(nodes[index - 1].inputs[first], nodes[index].outputs[0]) if len(nodes_list) > 1: links.new(nodes[index].inputs[second], nodes[nodes_list[i + 1][0]].outputs[0]) index -= 1 # set "last" of added nodes as active nodes.active = last_add for i, x, y in nodes_list: nodes[i].select = False return {'FINISHED'} class BatchChangeNodes(Operator, NodeToolBase): bl_idname = "node.batch_change" bl_label = "Batch Change" bl_description = "Batch Change Blend Type and Math Operation" bl_options = {'REGISTER', 'UNDO'} blend_type = EnumProperty( name="Blend Type", items=blend_types + navs, ) operation = EnumProperty( name="Operation", items=operations + navs, ) def execute(self, context): nodes, links = get_nodes_links(context) blend_type = self.blend_type operation = self.operation for node in context.selected_nodes: if node.type == 'MIX_RGB': if not blend_type in [nav[0] for nav in navs]: node.blend_type = blend_type else: if blend_type == 'NEXT': index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0] #index = blend_types.index(node.blend_type) if index == len(blend_types) - 1: node.blend_type = blend_types[0][0] else: node.blend_type = blend_types[index + 1][0] if blend_type == 'PREV': index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0] if index == 0: node.blend_type = blend_types[len(blend_types) - 1][0] else: node.blend_type = blend_types[index - 1][0] if node.type == 'MATH': if not operation in [nav[0] for nav in navs]: node.operation = operation else: if operation == 'NEXT': index = [i for i, entry in enumerate(operations) if node.operation in entry][0] #index = operations.index(node.operation) if index == len(operations) - 1: node.operation = operations[0][0] else: node.operation = operations[index + 1][0] if operation == 'PREV': index = [i for i, entry in enumerate(operations) if node.operation in entry][0] #index = operations.index(node.operation) if index == 0: node.operation = operations[len(operations) - 1][0] else: node.operation = operations[index - 1][0] return {'FINISHED'} class ChangeMixFactor(Operator, NodeToolBase): bl_idname = "node.factor" bl_label = "Change Factor" bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes" bl_options = {'REGISTER', 'UNDO'} # option: Change factor. # If option is 1.0 or 0.0 - set to 1.0 or 0.0 # Else - change factor by option value. option = FloatProperty() def execute(self, context): nodes, links = get_nodes_links(context) option = self.option selected = [] # entry = index for si, node in enumerate(nodes): if node.select: if node.type in {'MIX_RGB', 'MIX_SHADER'}: selected.append(si) for si in selected: fac = nodes[si].inputs[0] nodes[si].hide = False if option in {0.0, 1.0}: fac.default_value = option else: fac.default_value += option return {'FINISHED'} class NodesCopySettings(Operator): bl_idname = "node.copy_settings" bl_label = "Copy Settings" bl_description = "Copy Settings of Active Node to Selected Nodes" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): space = context.space_data valid = False if (space.type == 'NODE_EDITOR' and space.node_tree is not None and context.active_node is not None and context.active_node.type is not 'FRAME' ): valid = True return valid def execute(self, context): nodes, links = get_nodes_links(context) selected = [n for n in nodes if n.select] reselect = [] # duplicated nodes will be selected after execution active = nodes.active if active.select: reselect.append(active) for node in selected: if node.type == active.type and node != active: # duplicate active, relink links as in 'node', append copy to 'reselect', delete node bpy.ops.node.select_all(action='DESELECT') nodes.active = active active.select = True bpy.ops.node.duplicate() copied = nodes.active # Copied active should however inherit some properties from 'node' attributes = ( 'hide', 'show_preview', 'mute', 'label', 'use_custom_color', 'color', 'width', 'width_hidden', ) for attr in attributes: setattr(copied, attr, getattr(node, attr)) # Handle scenario when 'node' is in frame. 'copied' is in same frame then. if copied.parent: bpy.ops.node.parent_clear() locx = node.location.x locy = node.location.y # get absolute node location parent = node.parent while parent: locx += parent.location.x locy += parent.location.y parent = parent.parent copied.location = [locx, locy] # reconnect links from node to copied for i, input in enumerate(node.inputs): if input.links: link = input.links[0] links.new(link.from_socket, copied.inputs[i]) for out, output in enumerate(node.outputs): if output.links: out_links = output.links for link in out_links: links.new(copied.outputs[out], link.to_socket) bpy.ops.node.select_all(action='DESELECT') node.select = True bpy.ops.node.delete() reselect.append(copied) else: # If selected wasn't copied, need to reselect it afterwards. reselect.append(node) # clean up bpy.ops.node.select_all(action='DESELECT') for node in reselect: node.select = True nodes.active = active return {'FINISHED'} class NodesCopyLabel(Operator, NodeToolBase): bl_idname = "node.copy_label" bl_label = "Copy Label" bl_options = {'REGISTER', 'UNDO'} option = EnumProperty( name="option", description="Source of name of label", items=( ('FROM_ACTIVE', 'from active', 'from active node',), ('FROM_NODE', 'from node', 'from node linked to selected node'), ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'), ) ) def execute(self, context): nodes, links = get_nodes_links(context) option = self.option active = nodes.active if option == 'FROM_ACTIVE': if active: src_label = active.label for node in [n for n in nodes if n.select and nodes.active != n]: node.label = src_label elif option == 'FROM_NODE': selected = [n for n in nodes if n.select] for node in selected: for input in node.inputs: if input.links: src = input.links[0].from_node node.label = src.label break elif option == 'FROM_SOCKET': selected = [n for n in nodes if n.select] for node in selected: for input in node.inputs: if input.links: src = input.links[0].from_socket node.label = src.name break return {'FINISHED'} class NodesClearLabel(Operator, NodeToolBase): bl_idname = "node.clear_label" bl_label = "Clear Label" bl_options = {'REGISTER', 'UNDO'} option = BoolProperty() def execute(self, context): nodes, links = get_nodes_links(context) for node in [n for n in nodes if n.select]: node.label = '' return {'FINISHED'} def invoke(self, context, event): if self.option: return self.execute(context) else: return context.window_manager.invoke_confirm(self, event) class NodesAddTextureSetup(Operator): bl_idname = "node.add_texture" bl_label = "Texture Setup" bl_description = "Add Texture Node Setup to Selected Shaders" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): space = context.space_data valid = False if space.type == 'NODE_EDITOR': if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None: valid = True return valid def execute(self, context): nodes, links = get_nodes_links(context) active = nodes.active valid = False if active: if active.select: if active.type in { 'BSDF_ANISOTROPIC', 'BSDF_DIFFUSE', 'BSDF_GLOSSY', 'BSDF_GLASS', 'BSDF_REFRACTION', 'BSDF_TRANSLUCENT', 'BSDF_TRANSPARENT', 'BSDF_VELVET', 'EMISSION', 'AMBIENT_OCCLUSION', }: if not active.inputs[0].is_linked: valid = True if valid: locx = active.location.x locy = active.location.y tex = nodes.new('ShaderNodeTexImage') tex.location = [locx - 200.0, locy + 28.0] map = nodes.new('ShaderNodeMapping') map.location = [locx - 490.0, locy + 80.0] coord = nodes.new('ShaderNodeTexCoord') coord.location = [locx - 700, locy + 40.0] active.select = False nodes.active = tex links.new(tex.outputs[0], active.inputs[0]) links.new(map.outputs[0], tex.inputs[0]) links.new(coord.outputs[2], map.inputs[0]) return {'FINISHED'} class NodesAddReroutes(Operator, NodeToolBase): bl_idname = "node.add_reroutes" bl_label = "Add Reroutes" bl_description = "Add Reroutes to Outputs" bl_options = {'REGISTER', 'UNDO'} option = EnumProperty( name="option", items=[ ('ALL', 'to all', 'Add to all outputs'), ('LOOSE', 'to loose', 'Add only to loose outputs'), ('LINKED', 'to linked', 'Add only to linked outputs'), ] ) def execute(self, context): tree_type = context.space_data.node_tree.type option = self.option nodes, links = get_nodes_links(context) # output valid when option is 'all' or when 'loose' output has no links valid = False post_select = [] # nodes to be selected after execution # create reroutes and recreate links for node in [n for n in nodes if n.select]: if node.outputs: x = node.location.x y = node.location.y width = node.width # unhide 'REROUTE' nodes to avoid issues with location.y if node.type == 'REROUTE': node.hide = False # When node is hidden - width_hidden not usable. # Hack needed to calculate real width if node.hide: bpy.ops.node.select_all(action='DESELECT') helper = nodes.new('NodeReroute') helper.select = True node.select = True # resize node and helper to zero. Then check locations to calculate width bpy.ops.transform.resize(value=(0.0, 0.0, 0.0)) width = 2.0 * (helper.location.x - node.location.x) # restore node location node.location = x, y # delete helper node.select = False # only helper is selected now bpy.ops.node.delete() x = node.location.x + width + 20.0 if node.type != 'REROUTE': y -= 35.0 y_offset = -22.0 loc = x, y reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes for out_i, output in enumerate(node.outputs): pass_used = False # initial value to be analyzed if 'R_LAYERS' # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True if node.type != 'R_LAYERS': pass_used = True else: # if 'R_LAYERS' check if output represent used render pass node_scene = node.scene node_layer = node.layer # If output - "Alpha" is analyzed - assume it's used. Not represented in passes. if output.name == 'Alpha': pass_used = True else: # check entries in global 'rl_outputs' variable for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs: if output.name == out_name: pass_used = getattr(node_scene.render.layers[node_layer], render_pass) break if pass_used: valid = ((option == 'ALL') or (option == 'LOOSE' and not output.links) or (option == 'LINKED' and output.links)) # Add reroutes only if valid, but offset location in all cases. if valid: n = nodes.new('NodeReroute') nodes.active = n for link in output.links: links.new(n.outputs[0], link.to_socket) links.new(output, n.inputs[0]) n.location = loc post_select.append(n) reroutes_count += 1 y += y_offset loc = x, y # disselect the node so that after execution of script only newly created nodes are selected node.select = False # nicer reroutes distribution along y when node.hide if node.hide: y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0 for reroute in [r for r in nodes if r.select]: reroute.location.y -= y_translate for node in post_select: node.select = True return {'FINISHED'} class NodesSwap(Operator, NodeToolBase): bl_idname = "node.swap_nodes" bl_label = "Swap Nodes" bl_options = {'REGISTER', 'UNDO'} option = EnumProperty( items=[ ('CompositorNodeSwitch', 'Switch', 'Switch'), ('NodeReroute', 'Reroute', 'Reroute'), ('NodeMixRGB', 'Mix Node', 'Mix Node'), ('NodeMath', 'Math Node', 'Math Node'), ('CompositorNodeAlphaOver', 'Alpha Over', 'Alpha Over'), ('ShaderNodeBsdfTransparent', 'Transparent BSDF', 'Transparent BSDF'), ('ShaderNodeBsdfGlossy', 'Glossy BSDF', 'Glossy BSDF'), ('ShaderNodeBsdfGlass', 'Glass BSDF', 'Glass BSDF'), ('ShaderNodeBsdfDiffuse', 'Diffuse BSDF', 'Diffuse BSDF'), ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'), ('ShaderNodeEmission', 'Emission', 'Emission'), ('ShaderNodeBsdfVelvet', 'Velvet BSDF', 'Velvet BSDF'), ('ShaderNodeBsdfTranslucent', 'Translucent BSDF', 'Translucent BSDF'), ('ShaderNodeAmbientOcclusion', 'Ambient Occlusion', 'Ambient Occlusion'), ('ShaderNodeBackground', 'Background', 'Background'), ('ShaderNodeBsdfRefraction', 'Refraction BSDF', 'Refraction BSDF'), ('ShaderNodeBsdfAnisotropic', 'Anisotropic BSDF', 'Anisotropic BSDF'), ('ShaderNodeHoldout', 'Holdout', 'Holdout'), ('ShaderNodeAddShader', 'Add Shader', 'Add Shader'), ('ShaderNodeMixShader', 'Mix Shader', 'Mix Shader'), ] ) def execute(self, context): nodes, links = get_nodes_links(context) tree_type = context.space_data.tree_type if tree_type == 'CompositorNodeTree': prefix = 'Compositor' elif tree_type == 'ShaderNodeTree': prefix = 'Shader' option = self.option selected = [n for n in nodes if n.select] reselect = [] mode = None # will be used to set proper operation or blend type in new Math or Mix nodes. # regular_shaders - global list. Entry: (identifier, type, name for humans) # example: ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF') swap_shaders = option in (s[0] for s in regular_shaders) swap_merge_shaders = option in (s[0] for s in merge_shaders) if swap_shaders or swap_merge_shaders: # replace_types - list of node types that can be replaced using selected option shaders = regular_shaders + merge_shaders replace_types = [type[1] for type in shaders] new_type = option elif option == 'CompositorNodeSwitch': replace_types = ('REROUTE', 'MIX_RGB', 'MATH', 'ALPHAOVER') new_type = option elif option == 'NodeReroute': replace_types = ('SWITCH') new_type = option elif option == 'NodeMixRGB': replace_types = ('REROUTE', 'SWITCH', 'MATH', 'ALPHAOVER') new_type = prefix + option elif option == 'NodeMath': replace_types = ('REROUTE', 'SWITCH', 'MIX_RGB', 'ALPHAOVER') new_type = prefix + option elif option == 'CompositorNodeAlphaOver': replace_types = ('REROUTE', 'SWITCH', 'MATH', 'MIX_RGB') new_type = option for node in selected: if node.type in replace_types: hide = node.hide if node.type == 'REROUTE': hide = True new_node = nodes.new(new_type) # if swap Mix to Math of vice-verca - try to set blend type or operation accordingly if new_node.type in {'MIX_RGB', 'ALPHAOVER'}: new_node.inputs[0].default_value = 1.0 if node.type == 'MATH': if node.operation in [entry[0] for entry in blend_types]: if hasattr(new_node, 'blend_type'): new_node.blend_type = node.operation for i in range(2): new_node.inputs[i+1].default_value = [node.inputs[i].default_value] * 3 + [1.0] elif node.type in {'MIX_RGB', 'ALPHAOVER'}: for i in range(3): new_node.inputs[i].default_value = node.inputs[i].default_value elif new_node.type == 'MATH': if node.type in {'MIX_RGB', 'ALPHAOVER'}: if hasattr(node, 'blend_type'): if node.blend_type in [entry[0] for entry in operations]: new_node.operation = node.blend_type for i in range(2): channels = [] for c in range(3): channels.append(node.inputs[i+1].default_value[c]) new_node.inputs[i].default_value = max(channels) old_inputs_count = len(node.inputs) new_inputs_count = len(new_node.inputs) replace = [] # entries - pairs: old input index, new input index. if swap_shaders: for old_i, old_input in enumerate(node.inputs): for new_i, new_input in enumerate(new_node.inputs): if old_input.name == new_input.name: replace.append((old_i, new_i)) new_input.default_value = old_input.default_value break elif option == 'ShaderNodeAddShader': if node.type == 'ADD_SHADER': replace = ((0, 0), (1, 1)) elif node.type == 'MIX_SHADER': replace = ((1, 0), (2, 1)) elif option == 'ShaderNodeMixShader': if node.type == 'ADD_SHADER': replace = ((0, 1), (1, 2)) elif node.type == 'MIX_SHADER': replace = ((1, 1), (2, 2)) elif new_inputs_count == 1: replace = ((0, 0), ) elif new_inputs_count == 2: if old_inputs_count == 1: replace = ((0, 0), ) elif old_inputs_count == 2: replace = ((0, 0), (1, 1)) elif old_inputs_count == 3: replace = ((1, 0), (2, 1)) elif new_inputs_count == 3: if old_inputs_count == 1: replace = ((0, 1), ) elif old_inputs_count == 2: replace = ((0, 1), (1, 2)) elif old_inputs_count == 3: replace = ((0, 0), (1, 1), (2, 2)) if replace: for old_i, new_i in replace: if node.inputs[old_i].links: in_link = node.inputs[old_i].links[0] links.new(in_link.from_socket, new_node.inputs[new_i]) for out_link in node.outputs[0].links: links.new(new_node.outputs[0], out_link.to_socket) for attr in {'location', 'label', 'mute', 'show_preview', 'width_hidden', 'use_clamp'}: if hasattr(node, attr) and hasattr(new_node, attr): setattr(new_node, attr, getattr(node, attr)) new_node.hide = hide nodes.active = new_node reselect.append(new_node) bpy.ops.node.select_all(action="DESELECT") node.select = True bpy.ops.node.delete() else: reselect.append(node) for node in reselect: node.select = True return {'FINISHED'} class NodesLinkActiveToSelected(Operator): bl_idname = "node.link_active_to_selected" bl_label = "Link Active Node to Selected" bl_options = {'REGISTER', 'UNDO'} replace = BoolProperty() use_node_name = BoolProperty() use_outputs_names = BoolProperty() @classmethod def poll(cls, context): space = context.space_data valid = False if space.type == 'NODE_EDITOR': if space.node_tree is not None and context.active_node is not None: if context.active_node.select: valid = True return valid def execute(self, context): nodes, links = get_nodes_links(context) replace = self.replace use_node_name = self.use_node_name use_outputs_names = self.use_outputs_names active = nodes.active selected = [node for node in nodes if node.select and node != active] outputs = [] # Only usable outputs of active nodes will be stored here. for out in active.outputs: if active.type != 'R_LAYERS': outputs.append(out) else: # 'R_LAYERS' node type needs special handling. # outputs of 'R_LAYERS' are callable even if not seen in UI. # Only outputs that represent used passes should be taken into account # Check if pass represented by output is used. # global 'rl_outputs' list will be used for that for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs: pass_used = False # initial value. Will be set to True if pass is used if out.name == 'Alpha': # Alpha output is always present. Doesn't have representation in render pass. Assume it's used. pass_used = True elif out.name == out_name: # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers pass_used = getattr(active.scene.render.layers[active.layer], render_pass) break if pass_used: outputs.append(out) doit = True # Will be changed to False when links successfully added to previous output. for out in outputs: if doit: for node in selected: dst_name = node.name # Will be compared with src_name if needed. # When node has label - use it as dst_name if node.label: dst_name = node.label valid = True # Initial value. Will be changed to False if names don't match. src_name = dst_name # If names not used - this asignment will keep valid = True. if use_node_name: # Set src_name to source node name or label src_name = active.name if active.label: src_name = active.label elif use_outputs_names: src_name = (out.name, ) for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs: if out.name in {out_name, exr_name}: src_name = (out_name, exr_name) if dst_name not in src_name: valid = False if valid: for input in node.inputs: if input.type == out.type or node.type == 'REROUTE': if replace or not input.is_linked: links.new(out, input) if not use_node_name and not use_outputs_names: doit = False break return {'FINISHED'} class AlignNodes(Operator, NodeToolBase): bl_idname = "node.align_nodes" bl_label = "Align nodes" bl_options = {'REGISTER', 'UNDO'} # option: 'Vertically', 'Horizontally' option = EnumProperty( name="option", description="Direction", items=( ('AXIS_X', "Align Vertically", 'Align Vertically'), ('AXIS_Y', "Aligh Horizontally", 'Aligh Horizontally'), ) ) def execute(self, context): nodes, links = get_nodes_links(context) selected = [] # entry = [index, loc.x, loc.y, width, height] frames_reselect = [] # entry = frame node. will be used to reselect all selected frames active = nodes.active for i, node in enumerate(nodes): if node.select: if node.type == 'FRAME': node.select = False frames_reselect.append(i) else: locx = node.location.x locy = node.location.y parent = node.parent while parent is not None: locx += parent.location.x locy += parent.location.y parent = parent.parent selected.append([i, locx, locy]) count = len(selected) # add reroute node then scale all to 0.0 and calculate widths and heights of nodes if count > 1: # aligning makes sense only if at least 2 nodes are selected helper = nodes.new('NodeReroute') helper.select = True bpy.ops.transform.resize(value=(0.0, 0.0, 0.0)) # store helper's location for further calculations zero_x = helper.location.x zero_y = helper.location.y nodes.remove(helper) # helper is deleted but its location is stored # helper's width and height are 0.0. # Check loc of other nodes in relation to helper to calculate their dimensions # and append them to entries of "selected" total_w = 0.0 # total width of all nodes. Will be calculated later. total_h = 0.0 # total height of all nodes. Will be calculated later for j, [i, x, y] in enumerate(selected): locx = nodes[i].location.x locy = nodes[i].location.y # take node's parent (frame) into account. Get absolute location parent = nodes[i].parent while parent is not None: locx += parent.location.x locy += parent.location.y parent = parent.parent width = abs((zero_x - locx) * 2.0) height = abs((zero_y - locy) * 2.0) selected[j].append(width) # complete selected's entry for nodes[i] selected[j].append(height) # complete selected's entry for nodes[i] total_w += width # add nodes[i] width to total width of all nodes total_h += height # add nodes[i] height to total height of all nodes selected_sorted_x = sorted(selected, key=lambda k: (k[1], -k[2])) selected_sorted_y = sorted(selected, key=lambda k: (-k[2], k[1])) min_x = selected_sorted_x[0][1] # min loc.x min_x_loc_y = selected_sorted_x[0][2] # loc y of node with min loc x min_x_w = selected_sorted_x[0][3] # width of node with max loc x max_x = selected_sorted_x[count - 1][1] # max loc.x max_x_loc_y = selected_sorted_x[count - 1][2] # loc y of node with max loc.x max_x_w = selected_sorted_x[count - 1][3] # width of node with max loc.x min_y = selected_sorted_y[0][2] # min loc.y min_y_loc_x = selected_sorted_y[0][1] # loc.x of node with min loc.y min_y_h = selected_sorted_y[0][4] # height of node with min loc.y min_y_w = selected_sorted_y[0][3] # width of node with min loc.y max_y = selected_sorted_y[count - 1][2] # max loc.y max_y_loc_x = selected_sorted_y[count - 1][1] # loc x of node with max loc.y max_y_w = selected_sorted_y[count - 1][3] # width of node with max loc.y max_y_h = selected_sorted_y[count - 1][4] # height of node with max loc.y if self.option == 'AXIS_Y': # Horizontally. Equivelent of s -> x -> 0 with even spacing. loc_x = min_x #loc_y = (max_x_loc_y + min_x_loc_y) / 2.0 loc_y = (max_y - max_y_h / 2.0 + min_y - min_y_h / 2.0) / 2.0 offset_x = (max_x - min_x - total_w + max_x_w) / (count - 1) for i, x, y, w, h in selected_sorted_x: nodes[i].location.x = loc_x nodes[i].location.y = loc_y + h / 2.0 parent = nodes[i].parent while parent is not None: nodes[i].location.x -= parent.location.x nodes[i].location.y -= parent.location.y parent = parent.parent loc_x += offset_x + w else: # if self.option == 'AXIS_Y' #loc_x = (max_y_loc_x + max_y_w / 2.0 + min_y_loc_x + min_y_w / 2.0) / 2.0 loc_x = (max_x + max_x_w / 2.0 + min_x + min_x_w / 2.0) / 2.0 loc_y = min_y offset_y = (max_y - min_y + total_h - min_y_h) / (count - 1) for i, x, y, w, h in selected_sorted_y: nodes[i].location.x = loc_x - w / 2.0 nodes[i].location.y = loc_y parent = nodes[i].parent while parent is not None: nodes[i].location.x -= parent.location.x nodes[i].location.y -= parent.location.y parent = parent.parent loc_y += offset_y - h # reselect selected frames for i in frames_reselect: nodes[i].select = True # restore active node nodes.active = active return {'FINISHED'} class SelectParentChildren(Operator, NodeToolBase): bl_idname = "node.select_parent_child" bl_label = "Select Parent or Children" bl_options = {'REGISTER', 'UNDO'} option = EnumProperty( name="option", items=( ('PARENT', 'Select Parent', 'Select Parent Frame'), ('CHILD', 'Select Children', 'Select members of selected frame'), ) ) def execute(self, context): nodes, links = get_nodes_links(context) option = self.option selected = [node for node in nodes if node.select] if option == 'PARENT': for sel in selected: parent = sel.parent if parent: parent.select = True else: # option == 'CHILD' for sel in selected: children = [node for node in nodes if node.parent == sel] for kid in children: kid.select = True return {'FINISHED'} class DetachOutputs(Operator, NodeToolBase): bl_idname = "node.detach_outputs" bl_label = "Detach Outputs" bl_options = {'REGISTER', 'UNDO'} def execute(self, context): nodes, links = get_nodes_links(context) selected = context.selected_nodes bpy.ops.node.duplicate_move_keep_inputs() new_nodes = context.selected_nodes bpy.ops.node.select_all(action="DESELECT") for node in selected: node.select = True bpy.ops.node.delete_reconnect() for new_node in new_nodes: new_node.select = True bpy.ops.transform.translate('INVOKE_DEFAULT') return {'FINISHED'} class LinkToOutputNode(Operator, NodeToolBase): bl_idname = "node.link_to_output_node" bl_label = "Link to Output Node" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): space = context.space_data valid = False if (space.type == 'NODE_EDITOR' and space.node_tree is not None and context.active_node is not None ): valid = True return valid def execute(self, context): nodes, links = get_nodes_links(context) active = nodes.active output_node = None for node in nodes: if (node.type == 'OUTPUT_MATERIAL' or\ node.type == 'OUTPUT_WORLD' or\ node.type == 'OUTPUT_LAMP' or\ node.type == 'COMPOSITE'): output_node = node break if not output_node: bpy.ops.node.select_all(action="DESELECT") type = context.space_data.tree_type if type == 'ShaderNodeTree': output_node = nodes.new('ShaderNodeOutputMaterial') elif type == 'CompositorNodeTree': output_node = nodes.new('CompositorNodeComposite') output_node.location = active.location + Vector((300.0, 0.0)) nodes.active = output_node if (output_node and active.outputs): output_index = 0 for i, output in enumerate(active.outputs): if output.type == output_node.inputs[0].type: output_index = i break links.new(active.outputs[output_index], output_node.inputs[0]) return {'FINISHED'} ############################################################# # P A N E L S ############################################################# class EfficiencyToolsPanel(Panel, NodeToolBase): bl_idname = "NODE_PT_efficiency_tools" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_label = "Efficiency Tools (Ctrl-SPACE)" def draw(self, context): type = context.space_data.tree_type layout = self.layout box = layout.box() box.menu(MergeNodesMenu.bl_idname) if type == 'ShaderNodeTree': box.operator(NodesAddTextureSetup.bl_idname, text="Add Image Texture (Ctrl T)") box.menu(BatchChangeNodesMenu.bl_idname, text="Batch Change...") box.menu(NodeAlignMenu.bl_idname, text="Align Nodes (Shift =)") box.menu(CopyToSelectedMenu.bl_idname, text="Copy to Selected (Shift-C)") box.operator(NodesClearLabel.bl_idname).option = True box.operator(DetachOutputs.bl_idname) box.menu(AddReroutesMenu.bl_idname, text="Add Reroutes ( / )") box.menu(NodesSwapMenu.bl_idname, text="Swap Nodes (Shift-S)") box.menu(LinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected ( \\ )") box.operator(LinkToOutputNode.bl_idname) ############################################################# # M E N U S ############################################################# class EfficiencyToolsMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_node_tools_menu" bl_label = "Efficiency Tools" def draw(self, context): type = context.space_data.tree_type layout = self.layout layout.menu(MergeNodesMenu.bl_idname, text="Merge Selected Nodes") if type == 'ShaderNodeTree': layout.operator(NodesAddTextureSetup.bl_idname, text="Add Image Texture with coordinates") layout.menu(BatchChangeNodesMenu.bl_idname, text="Batch Change") layout.menu(NodeAlignMenu.bl_idname, text="Align Nodes") layout.menu(CopyToSelectedMenu.bl_idname, text="Copy to Selected") layout.operator(NodesClearLabel.bl_idname).option = True layout.operator(DetachOutputs.bl_idname) layout.menu(AddReroutesMenu.bl_idname, text="Add Reroutes") layout.menu(NodesSwapMenu.bl_idname, text="Swap Nodes") layout.menu(LinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected") layout.operator(LinkToOutputNode.bl_idname) class MergeNodesMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_merge_nodes_menu" bl_label = "Merge Selected Nodes" def draw(self, context): type = context.space_data.tree_type layout = self.layout if type == 'ShaderNodeTree': layout.menu(MergeShadersMenu.bl_idname, text="Use Shaders") layout.menu(MergeMixMenu.bl_idname, text="Use Mix Nodes") layout.menu(MergeMathMenu.bl_idname, text="Use Math Nodes") class MergeShadersMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_merge_shaders_menu" bl_label = "Merge Selected Nodes using Shaders" def draw(self, context): layout = self.layout for type in merge_shaders_types: props = layout.operator(MergeNodes.bl_idname, text=type) props.mode = type props.merge_type = 'SHADER' class MergeMixMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_merge_mix_menu" bl_label = "Merge Selected Nodes using Mix" def draw(self, context): layout = self.layout for type, name, description in blend_types: props = layout.operator(MergeNodes.bl_idname, text=name) props.mode = type props.merge_type = 'MIX' class MergeMathMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_merge_math_menu" bl_label = "Merge Selected Nodes using Math" def draw(self, context): layout = self.layout for type, name, description in operations: props = layout.operator(MergeNodes.bl_idname, text=name) props.mode = type props.merge_type = 'MATH' class BatchChangeNodesMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_batch_change_nodes_menu" bl_label = "Batch Change Selected Nodes" def draw(self, context): layout = self.layout layout.menu(BatchChangeBlendTypeMenu.bl_idname) layout.menu(BatchChangeOperationMenu.bl_idname) class BatchChangeBlendTypeMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_batch_change_blend_type_menu" bl_label = "Batch Change Blend Type" def draw(self, context): layout = self.layout for type, name, description in blend_types: props = layout.operator(BatchChangeNodes.bl_idname, text=name) props.blend_type = type props.operation = 'CURRENT' class BatchChangeOperationMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_batch_change_operation_menu" bl_label = "Batch Change Math Operation" def draw(self, context): layout = self.layout for type, name, description in operations: props = layout.operator(BatchChangeNodes.bl_idname, text=name) props.blend_type = 'CURRENT' props.operation = type class CopyToSelectedMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_copy_node_properties_menu" bl_label = "Copy to Selected" def draw(self, context): layout = self.layout layout.operator(NodesCopySettings.bl_idname, text="Settings from Active") layout.menu(CopyLabelMenu.bl_idname) class CopyLabelMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_copy_label_menu" bl_label = "Copy Label" def draw(self, context): layout = self.layout layout.operator(NodesCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE' layout.operator(NodesCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE' layout.operator(NodesCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET' class AddReroutesMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_add_reroutes_menu" bl_label = "Add Reroutes" bl_description = "Add Reroute Nodes to Selected Nodes' Outputs" def draw(self, context): layout = self.layout layout.operator(NodesAddReroutes.bl_idname, text="to All Outputs").option = 'ALL' layout.operator(NodesAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE' layout.operator(NodesAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED' class NodesSwapMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_swap_menu" bl_label = "Swap Nodes" def draw(self, context): type = context.space_data.tree_type layout = self.layout if type == 'ShaderNodeTree': layout.menu(ShadersSwapMenu.bl_idname, text="Swap Shaders") layout.operator(NodesSwap.bl_idname, text="Change to Mix Nodes").option = 'NodeMixRGB' layout.operator(NodesSwap.bl_idname, text="Change to Math Nodes").option = 'NodeMath' if type == 'CompositorNodeTree': layout.operator(NodesSwap.bl_idname, text="Change to Alpha Over").option = 'CompositorNodeAlphaOver' if type == 'CompositorNodeTree': layout.operator(NodesSwap.bl_idname, text="Change to Switches").option = 'CompositorNodeSwitch' layout.operator(NodesSwap.bl_idname, text="Change to Reroutes").option = 'NodeReroute' class ShadersSwapMenu(Menu): bl_idname = "NODE_MT_shaders_swap_menu" bl_label = "Swap Shaders" @classmethod def poll(cls, context): space = context.space_data valid = False if space.type == 'NODE_EDITOR': if space.tree_type == 'ShaderNodeTree' and space.node_tree is not None: valid = True return valid def draw(self, context): layout = self.layout shaders = regular_shaders + merge_shaders for opt, type, txt in shaders: layout.operator(NodesSwap.bl_idname, text=txt).option = opt class LinkActiveToSelectedMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_link_active_to_selected_menu" bl_label = "Link Active to Selected" def draw(self, context): layout = self.layout layout.menu(LinkStandardMenu.bl_idname) layout.menu(LinkUseNodeNameMenu.bl_idname) layout.menu(LinkUseOutputsNamesMenu.bl_idname) class LinkStandardMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_link_standard_menu" bl_label = "To All Selected" def draw(self, context): layout = self.layout props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") props.replace = False props.use_node_name = False props.use_outputs_names = False props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links") props.replace = True props.use_node_name = False props.use_outputs_names = False class LinkUseNodeNameMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_link_use_node_name_menu" bl_label = "Use Node Name/Label" def draw(self, context): layout = self.layout props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") props.replace = False props.use_node_name = True props.use_outputs_names = False props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links") props.replace = True props.use_node_name = True props.use_outputs_names = False class LinkUseOutputsNamesMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_link_use_outputs_names_menu" bl_label = "Use Outputs Names" def draw(self, context): layout = self.layout props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") props.replace = False props.use_node_name = False props.use_outputs_names = True props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links") props.replace = True props.use_node_name = False props.use_outputs_names = True class NodeAlignMenu(Menu, NodeToolBase): bl_idname = "NODE_MT_node_align_menu" bl_label = "Align Nodes" def draw(self, context): layout = self.layout layout.operator(AlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X' layout.operator(AlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y' ############################################################# # MENU ITEMS ############################################################# def select_parent_children_buttons(self, context): layout = self.layout layout.operator(SelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD' layout.operator(SelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT' ############################################################# # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS ############################################################# addon_keymaps = [] # kmi_defs entry: (identifier, key, CTRL, SHIFT, ALT, props) # props entry: (property name, property value) kmi_defs = ( # MERGE NODES # MergeNodes with Ctrl (AUTO). (MergeNodes.bl_idname, 'NUMPAD_0', True, False, False, (('mode', 'MIX'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'ZERO', True, False, False, (('mode', 'MIX'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, False, (('mode', 'ADD'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'EQUAL', True, False, False, (('mode', 'ADD'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, False, (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'EIGHT', True, False, False, (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, False, (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'MINUS', True, False, False, (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, False, (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'SLASH', True, False, False, (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),)), (MergeNodes.bl_idname, 'COMMA', True, False, False, (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'PERIOD', True, False, False, (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),)), # MergeNodes with Ctrl Alt (MIX) (MergeNodes.bl_idname, 'NUMPAD_0', True, False, True, (('mode', 'MIX'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'ZERO', True, False, True, (('mode', 'MIX'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, False, True, (('mode', 'ADD'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'EQUAL', True, False, True, (('mode', 'ADD'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, False, True, (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'EIGHT', True, False, True, (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, False, True, (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'MINUS', True, False, True, (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, False, True, (('mode', 'DIVIDE'), ('merge_type', 'MIX'),)), (MergeNodes.bl_idname, 'SLASH', True, False, True, (('mode', 'DIVIDE'), ('merge_type', 'MIX'),)), # MergeNodes with Ctrl Shift (MATH) (MergeNodes.bl_idname, 'NUMPAD_PLUS', True, True, False, (('mode', 'ADD'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'EQUAL', True, True, False, (('mode', 'ADD'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'NUMPAD_ASTERIX', True, True, False, (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'EIGHT', True, True, False, (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'NUMPAD_MINUS', True, True, False, (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'MINUS', True, True, False, (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'NUMPAD_SLASH', True, True, False, (('mode', 'DIVIDE'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'SLASH', True, True, False, (('mode', 'DIVIDE'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'COMMA', True, True, False, (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),)), (MergeNodes.bl_idname, 'PERIOD', True, True, False, (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),)), # BATCH CHANGE NODES # BatchChangeNodes with Alt (BatchChangeNodes.bl_idname, 'NUMPAD_0', False, False, True, (('blend_type', 'MIX'), ('operation', 'CURRENT'),)), (BatchChangeNodes.bl_idname, 'ZERO', False, False, True, (('blend_type', 'MIX'), ('operation', 'CURRENT'),)), (BatchChangeNodes.bl_idname, 'NUMPAD_PLUS', False, False, True, (('blend_type', 'ADD'), ('operation', 'ADD'),)), (BatchChangeNodes.bl_idname, 'EQUAL', False, False, True, (('blend_type', 'ADD'), ('operation', 'ADD'),)), (BatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', False, False, True, (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),)), (BatchChangeNodes.bl_idname, 'EIGHT', False, False, True, (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),)), (BatchChangeNodes.bl_idname, 'NUMPAD_MINUS', False, False, True, (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),)), (BatchChangeNodes.bl_idname, 'MINUS', False, False, True, (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),)), (BatchChangeNodes.bl_idname, 'NUMPAD_SLASH', False, False, True, (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),)), (BatchChangeNodes.bl_idname, 'SLASH', False, False, True, (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),)), (BatchChangeNodes.bl_idname, 'COMMA', False, False, True, (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),)), (BatchChangeNodes.bl_idname, 'PERIOD', False, False, True, (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),)), (BatchChangeNodes.bl_idname, 'DOWN_ARROW', False, False, True, (('blend_type', 'NEXT'), ('operation', 'NEXT'),)), (BatchChangeNodes.bl_idname, 'UP_ARROW', False, False, True, (('blend_type', 'PREV'), ('operation', 'PREV'),)), # LINK ACTIVE TO SELECTED # Don't use names, don't replace links (K) (NodesLinkActiveToSelected.bl_idname, 'K', False, False, False, (('replace', False), ('use_node_name', False), ('use_outputs_names', False),)), # Don't use names, replace links (Shift K) (NodesLinkActiveToSelected.bl_idname, 'K', False, True, False, (('replace', True), ('use_node_name', False), ('use_outputs_names', False),)), # Use node name, don't replace links (') (NodesLinkActiveToSelected.bl_idname, 'QUOTE', False, False, False, (('replace', False), ('use_node_name', True), ('use_outputs_names', False),)), # Don't use names, replace links (') (NodesLinkActiveToSelected.bl_idname, 'QUOTE', False, True, False, (('replace', True), ('use_node_name', True), ('use_outputs_names', False),)), (NodesLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, False, False, (('replace', False), ('use_node_name', False), ('use_outputs_names', True),)), # Don't use names, replace links (') (NodesLinkActiveToSelected.bl_idname, 'SEMI_COLON', False, True, False, (('replace', True), ('use_node_name', False), ('use_outputs_names', True),)), # CHANGE MIX FACTOR (ChangeMixFactor.bl_idname, 'LEFT_ARROW', False, False, True, (('option', -0.1),)), (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, False, True, (('option', 0.1),)), (ChangeMixFactor.bl_idname, 'LEFT_ARROW', False, True, True, (('option', -0.01),)), (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', False, True, True, (('option', 0.01),)), (ChangeMixFactor.bl_idname, 'LEFT_ARROW', True, True, True, (('option', 0.0),)), (ChangeMixFactor.bl_idname, 'RIGHT_ARROW', True, True, True, (('option', 1.0),)), (ChangeMixFactor.bl_idname, 'NUMPAD_0', True, True, True, (('option', 0.0),)), (ChangeMixFactor.bl_idname, 'ZERO', True, True, True, (('option', 0.0),)), (ChangeMixFactor.bl_idname, 'NUMPAD_1', True, True, True, (('option', 1.0),)), (ChangeMixFactor.bl_idname, 'ONE', True, True, True, (('option', 1.0),)), # CLEAR LABEL (Alt L) (NodesClearLabel.bl_idname, 'L', False, False, True, (('option', False),)), # DETACH OUTPUTS (Alt Shift D) (DetachOutputs.bl_idname, 'D', False, True, True, None), # LINK TO OUTPUT NODE (O) (LinkToOutputNode.bl_idname, 'O', False, False, False, None), # SELECT PARENT/CHILDREN # Select Children (SelectParentChildren.bl_idname, 'RIGHT_BRACKET', False, False, False, (('option', 'CHILD'),)), # Select Parent (SelectParentChildren.bl_idname, 'LEFT_BRACKET', False, False, False, (('option', 'PARENT'),)), # Add Texture Setup (NodesAddTextureSetup.bl_idname, 'T', True, False, False, None), # Copy Label from active to selected (NodesCopyLabel.bl_idname, 'V', False, True, False, (('option', 'FROM_ACTIVE'),)), # MENUS ('wm.call_menu', 'SPACE', True, False, False, (('name', EfficiencyToolsMenu.bl_idname),)), ('wm.call_menu', 'SLASH', False, False, False, (('name', AddReroutesMenu.bl_idname),)), ('wm.call_menu', 'NUMPAD_SLASH', False, False, False, (('name', AddReroutesMenu.bl_idname),)), ('wm.call_menu', 'EQUAL', False, True, False, (('name', NodeAlignMenu.bl_idname),)), ('wm.call_menu', 'BACK_SLASH', False, False, False, (('name', LinkActiveToSelectedMenu.bl_idname),)), ('wm.call_menu', 'C', False, True, False, (('name', CopyToSelectedMenu.bl_idname),)), ('wm.call_menu', 'S', False, True, False, (('name', NodesSwapMenu.bl_idname),)), ) def register(): bpy.utils.register_module(__name__) km = bpy.context.window_manager.keyconfigs.addon.keymaps.new(name='Node Editor', space_type="NODE_EDITOR") for (identifier, key, CTRL, SHIFT, ALT, props) in kmi_defs: kmi = km.keymap_items.new(identifier, key, 'PRESS', ctrl=CTRL, shift=SHIFT, alt=ALT) if props: for prop, value in props: setattr(kmi.properties, prop, value) addon_keymaps.append((km, kmi)) # menu items bpy.types.NODE_MT_select.append(select_parent_children_buttons) def unregister(): bpy.utils.unregister_module(__name__) bpy.types.NODE_MT_select.remove(select_parent_children_buttons) for km, kmi in addon_keymaps: km.keymap_items.remove(kmi) addon_keymaps.clear() if __name__ == "__main__": register()