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:
authorBartek Skorupa <bartekskorupa@bartekskorupa.com>2013-03-28 01:37:36 +0400
committerBartek Skorupa <bartekskorupa@bartekskorupa.com>2013-03-28 01:37:36 +0400
commit5671058596314304155118c42305a10bf72a8de0 (patch)
tree3c0b5f98ec0222d219f74f42880ba71680e95a7c /node_efficiency_tools.py
parent86b21848fbbbc4ac114e29f6cc14c9bffb23067e (diff)
Committing Nodes Efficiency Tools to trunk.
'Nodes Efficiency Tools' is a set of several tools that can speed up working with nodes, both compositing and Cycles shaders. They automate several tasks that by default require many clicks, drags etc. All of the tools can be accessed via additional panel in node editor properties toolset or by hitting Ctrl-Space keyboard shortcut. Most of the options however have their own keyboard shortcuts and using them is much more efficient. You can find all of the info on the wikipage: http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/Nodes/Nodes_Efficiency_Tools Available options are as follows: Merge selected nodes: Imagine that you want to 'add' images represented by outputs of some nodes. Standard way of doing it is to add MIX node, change blend type to 'Add' and link outputs of the nodes accordingly. With Nodes Efficiency Tools you can select the nodes and hit (Ctrl +) keyboard shortcut. Proper setup of nodes will be added and links will be made. Other shortcuts are used for other blend types, you can also use 'Math' nodes for merging and different shortcuts allow to do so. Batch Change Blend Type or Math Operation: Hit (Alt-UP ARROW) or (Alt-Down ARROW) to change blend types or math operations of selected nodes. It's often much quicker than standard way of doing it. Use additional menu to select the blend type from the list and change types of all selected nodes at once. Change Factor of Mix Nodes or Mix Shaders: (Alt-LEFT ARROW), (Alt-RIGHT ARROW) shortcuts allow to change factors of selected Mix nodes or Mix Shader nodes by 0.1. More precision is gained when we add Shift to the key combination. Align Nodes: We can align nodes nicer than by hitting S-X-zero. Even distribution of aligned nodes can be achieved easily. (Shift =) Copy Settings: We can copy settings from Active Node to all selected ones. May be helpful if you want to use exactly the same RGB Curves nodes in different places of your node tree. This option works with all node types. Copy Label: We can copy labels from one node to other nodes with one click. Swap Nodes: It's possible to change the node to other type. Swap easily between 'Mix' and 'Alpha Over' nodes for example or easily change 'Diffuse' Shader to 'Glossy' Shader. Link Active to Selected: This option allows to link active node to all selected ones basing on several criteria. May be extremely helpful when for example re-linking image sources. We can easily change input Render Layers node to input image (MultiLayerEXR) preserving nodes setup. All of the options are explained in details in the video tutorial embedded on the wikipage. [[Split portion of a mixed commit.]]
Diffstat (limited to 'node_efficiency_tools.py')
-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()