diff --git a/node_efficiency_tools.py b/node_efficiency_tools.py new file mode 100644 index 0000000000000000000000000000000000000000..901c25a15746628571e0382e172f4d44b1bd8a1e --- /dev/null +++ b/node_efficiency_tools.py @@ -0,0 +1,1511 @@ +# ##### 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, 20), + 'blender': (2, 6, 6), + '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/?func=detail&atid=468&aid=33543&group_id=153", + 'category': "Node", + } + +import bpy +from bpy.types import Operator, Panel, Menu +from bpy.props import FloatProperty, EnumProperty, BoolProperty + +################# +# 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 = ('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'), + ('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'), + ) + + +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, 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, 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 + # add link from "first" selected and "first" add node + links.new(nodes[nodes_list[0][0]].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 = nodes[count_before] + 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'), + ('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'), + ] + ) + + 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) + if swap_shaders: + # replace_types - list of node types that can be replaced using selected option + replace_types = [type[1] for type in regular_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 == 'MIX_RGB': + if node.type == 'MATH': + if node.operation in [entry[0] for entry in blend_types]: + new_node.blend_type = node.operation + elif new_node.type == 'MATH': + if node.type == 'MIX_RGB': + if node.blend_type in [entry[0] for entry in operations]: + new_node.operation = node.blend_type + old_inputs_count = len(node.inputs) + new_inputs_count = len(new_node.inputs) + if swap_shaders: + replace = [] + 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)) + break + elif new_inputs_count == 1: + replace = ((0, 0), ) # old input 0 (first of the entry) will be replaced by new input 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) + new_node.location = node.location + new_node.label = node.label + new_node.hide = hide + new_node.mute = node.mute + new_node.show_preview = node.show_preview + new_node.width_hidden = node.width_hidden + 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_X': + 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'} + + +############################################################# +# 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.menu(AddReroutesMenu.bl_idname, text="Add Reroutes") + box.menu(NodesSwapMenu.bl_idname, text="Swap Nodes") + box.menu(LinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected") + + +############################################################# +# 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.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") + + +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: + 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 + for opt, type, txt in regular_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(LinkUseNamesMenu.bl_idname, text="Use names/labels") + + +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 (Shift-F)") + props.replace = False + props.use_node_name = False + props.use_outputs_names = False + props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Replace Links (Ctrl-Shift-F)") + props.replace = True + props.use_node_name = False + props.use_outputs_names = False + + +class LinkUseNamesMenu(Menu, NodeToolBase): + bl_idname = "NODE_MT_link_use_names_menu" + bl_label = "Link Active to Selected" + + def draw(self, context): + layout = self.layout + layout.menu(LinkUseNodeNameMenu.bl_idname, text="Use Node Name/Label") + layout.menu(LinkUseOutputsNamesMenu.bl_idname, text="Use Outputs Names") + + +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="Replace Links") + props.replace = True + props.use_node_name = True + props.use_outputs_names = False + props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") + props.replace = False + 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="Replace Links") + props.replace = True + props.use_node_name = False + props.use_outputs_names = True + props = layout.operator(NodesLinkActiveToSelected.bl_idname, text="Don't Replace Links") + props.replace = False + 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, replace links (Ctrl Shift F) + (NodesLinkActiveToSelected.bl_idname, 'F', True, True, False, + (('replace', True), ('use_node_name', False), ('use_outputs_names', False),)), + # Don't use names, don't replace links (Shift F) + (NodesLinkActiveToSelected.bl_idname, 'F', False, True, False, + (('replace', False), ('use_node_name', False), ('use_outputs_names', False),)), + # 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),)), + # 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'),)), + (NodesAddTextureSetup.bl_idname, 'T', True, False, False, None), + # 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', 'F', False, False, True, (('name', LinkUseNamesMenu.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()