Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--node_efficiency_tools.py1511
1 files changed, 1511 insertions, 0 deletions
diff --git a/node_efficiency_tools.py b/node_efficiency_tools.py
new file mode 100644
index 00000000..901c25a1
--- /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()