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:
authorGreg Zaal <gregzzmail@gmail.com>2015-01-21 18:17:59 +0300
committerGreg Zaal <gregzzmail@gmail.com>2015-01-21 18:18:42 +0300
commit25bb054adcb4588c035fdc2229185f704e486239 (patch)
tree923bcaf65f7aee31aa9072a1d4bd0182da605481 /node_wrangler.py
parent5be0ed31f09a0c3696973a3815db306ed08905bf (diff)
Rename `node_efficiency_tools.py` to `node_wrangler.py`
Diffstat (limited to 'node_wrangler.py')
-rw-r--r--node_wrangler.py4137
1 files changed, 4137 insertions, 0 deletions
diff --git a/node_wrangler.py b/node_wrangler.py
new file mode 100644
index 00000000..06b7f742
--- /dev/null
+++ b/node_wrangler.py
@@ -0,0 +1,4137 @@
+# ##### 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": "Node Wrangler",
+ "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig",
+ "version": (3, 20),
+ "blender": (2, 72, 0),
+ "location": "Node Editor Properties Panel or Ctrl-Space",
+ "description": "Various tools to enhance and speed up node-based workflow",
+ "warning": "",
+ "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
+ "Scripts/Nodes/Nodes_Efficiency_Tools",
+ "category": "Node",
+}
+
+import bpy, blf, bgl
+from bpy.types import Operator, Panel, Menu
+from bpy.props import FloatProperty, EnumProperty, BoolProperty, IntProperty, StringProperty, FloatVectorProperty, CollectionProperty
+from bpy_extras.io_utils import ImportHelper
+from mathutils import Vector
+from math import cos, sin, pi, hypot
+from os import listdir
+from glob import glob
+
+#################
+# 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_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', False, True),
+ ('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', False, True),
+ ('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
+ ('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),
+)
+
+# shader nodes
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_input_nodes_props = (
+ ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
+ ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
+ ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
+ ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
+ ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
+ ('ShaderNodeRGB', 'RGB', 'RGB'),
+ ('ShaderNodeValue', 'VALUE', 'Value'),
+ ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
+ ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
+ ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
+ ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
+ ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
+ ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
+ ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
+ ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_output_nodes_props = (
+ ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
+ ('ShaderNodeOutputLamp', 'OUTPUT_LAMP', 'Lamp Output'),
+ ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_shader_nodes_props = (
+ ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
+ ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
+ ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
+ ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
+ ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
+ ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
+ ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
+ ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
+ ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
+ ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
+ ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
+ ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
+ ('ShaderNodeEmission', 'EMISSION', 'Emission'),
+ ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
+ ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
+ ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
+ ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
+ ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
+ ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_texture_nodes_props = (
+ ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image'),
+ ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment'),
+ ('ShaderNodeTexSky', 'TEX_SKY', 'Sky'),
+ ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise'),
+ ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave'),
+ ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
+ ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
+ ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient'),
+ ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic'),
+ ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker'),
+ ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick')
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_color_nodes_props = (
+ ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
+ ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
+ ('ShaderNodeInvert', 'INVERT', 'Invert'),
+ ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
+ ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
+ ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
+ ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_vector_nodes_props = (
+ ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
+ ('ShaderNodeBump', 'BUMP', 'Bump'),
+ ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
+ ('ShaderNodeNormal', 'NORMAL', 'Normal'),
+ ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
+ ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_converter_nodes_props = (
+ ('ShaderNodeMath', 'MATH', 'Math'),
+ ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
+ ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
+ ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
+ ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
+ ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
+ ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
+ ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
+ ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
+ ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
+ ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
+ ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+shaders_layout_nodes_props = (
+ ('NodeFrame', 'FRAME', 'Frame'),
+ ('NodeReroute', 'REROUTE', 'Reroute'),
+)
+
+# compositing nodes
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_input_nodes_props = (
+ ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
+ ('CompositorNodeImage', 'IMAGE', 'Image'),
+ ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
+ ('CompositorNodeMask', 'MASK', 'Mask'),
+ ('CompositorNodeRGB', 'RGB', 'RGB'),
+ ('CompositorNodeValue', 'VALUE', 'Value'),
+ ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
+ ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
+ ('CompositorNodeTime', 'TIME', 'Time'),
+ ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_output_nodes_props = (
+ ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
+ ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
+ ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
+ ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
+ ('CompositorNodeLevels', 'LEVELS', 'Levels'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_color_nodes_props = (
+ ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
+ ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
+ ('CompositorNodeInvert', 'INVERT', 'Invert'),
+ ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
+ ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
+ ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
+ ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
+ ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
+ ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
+ ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
+ ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
+ ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_converter_nodes_props = (
+ ('CompositorNodeMath', 'MATH', 'Math'),
+ ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
+ ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
+ ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
+ ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
+ ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
+ ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
+ ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
+ ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
+ ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
+ ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
+ ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
+ ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
+ ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_filter_nodes_props = (
+ ('CompositorNodeBlur', 'BLUR', 'Blur'),
+ ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
+ ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
+ ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
+ ('CompositorNodeFilter', 'FILTER', 'Filter'),
+ ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
+ ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
+ ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
+ ('CompositorNodeGlare', 'GLARE', 'Glare'),
+ ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
+ ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
+ ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
+ ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_vector_nodes_props = (
+ ('CompositorNodeNormal', 'NORMAL', 'Normal'),
+ ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
+ ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
+ ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
+ ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_matte_nodes_props = (
+ ('CompositorNodeKeying', 'KEYING', 'Keying'),
+ ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
+ ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
+ ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
+ ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
+ ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
+ ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
+ ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
+ ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
+ ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
+ ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
+ ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_distort_nodes_props = (
+ ('CompositorNodeScale', 'SCALE', 'Scale'),
+ ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
+ ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
+ ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
+ ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
+ ('CompositorNodeFlip', 'FLIP', 'Flip'),
+ ('CompositorNodeCrop', 'CROP', 'Crop'),
+ ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
+ ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
+ ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
+ ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
+ ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
+ ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
+)
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+compo_layout_nodes_props = (
+ ('NodeFrame', 'FRAME', 'Frame'),
+ ('NodeReroute', 'REROUTE', 'Reroute'),
+ ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
+)
+# Blender Render material nodes
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+blender_mat_input_nodes_props = (
+ ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
+ ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
+ ('ShaderNodeLampData', 'LAMP', 'Lamp Data'),
+ ('ShaderNodeValue', 'VALUE', 'Value'),
+ ('ShaderNodeRGB', 'RGB', 'RGB'),
+ ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
+ ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
+ ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+blender_mat_output_nodes_props = (
+ ('ShaderNodeOutput', 'OUTPUT', 'Output'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+blender_mat_color_nodes_props = (
+ ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
+ ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
+ ('ShaderNodeInvert', 'INVERT', 'Invert'),
+ ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+blender_mat_vector_nodes_props = (
+ ('ShaderNodeNormal', 'NORMAL', 'Normal'),
+ ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
+ ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+blender_mat_converter_nodes_props = (
+ ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
+ ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
+ ('ShaderNodeMath', 'MATH', 'Math'),
+ ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
+ ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
+ ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
+ ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
+ ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
+ ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+blender_mat_layout_nodes_props = (
+ ('NodeReroute', 'REROUTE', 'Reroute'),
+)
+
+# Texture Nodes
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_input_nodes_props = (
+ ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
+ ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
+ ('TextureNodeTexture', 'TEXTURE', 'Texture'),
+ ('TextureNodeImage', 'IMAGE', 'Image'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_output_nodes_props = (
+ ('TextureNodeOutput', 'OUTPUT', 'Output'),
+ ('TextureNodeViewer', 'VIEWER', 'Viewer'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_color_nodes_props = (
+ ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
+ ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
+ ('TextureNodeInvert', 'INVERT', 'Invert'),
+ ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
+ ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
+ ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_pattern_nodes_props = (
+ ('TextureNodeChecker', 'CHECKER', 'Checker'),
+ ('TextureNodeBricks', 'BRICKS', 'Bricks'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_textures_nodes_props = (
+ ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
+ ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
+ ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
+ ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
+ ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
+ ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
+ ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
+ ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
+ ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
+ ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_converter_nodes_props = (
+ ('TextureNodeMath', 'MATH', 'Math'),
+ ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
+ ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
+ ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
+ ('TextureNodeDistance', 'DISTANCE', 'Distance'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_distort_nodes_props = (
+ ('TextureNodeScale', 'SCALE', 'Scale'),
+ ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
+ ('TextureNodeRotate', 'ROTATE', 'Rotate'),
+ ('TextureNodeAt', 'AT', 'At'),
+)
+
+# (rna_type.identifier, type, rna_type.name)
+# Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
+texture_layout_nodes_props = (
+ ('NodeReroute', 'REROUTE', 'Reroute'),
+)
+
+# list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
+# used list, not tuple for easy merging with other lists.
+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.
+# used list, not tuple for easy merging with other lists.
+operations = [
+ ('ADD', 'Add', 'Add Mode'),
+ ('SUBTRACT', 'Subtract', 'Subtract Mode'),
+ ('MULTIPLY', 'Multiply', 'Multiply 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 Than Mode'),
+ ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
+ ('MODULO', 'Modulo', 'Modulo Mode'),
+ ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
+]
+
+# in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
+# used list, not tuple for easy merging with other lists.
+navs = [
+ ('CURRENT', 'Current', 'Leave at current state'),
+ ('NEXT', 'Next', 'Next blend type/operation'),
+ ('PREV', 'Prev', 'Previous blend type/operation'),
+]
+
+draw_color_sets = {
+ "red_white": (
+ (1.0, 1.0, 1.0, 0.7),
+ (1.0, 0.0, 0.0, 0.7),
+ (0.8, 0.2, 0.2, 1.0)
+ ),
+ "green": (
+ (0.0, 0.0, 0.0, 1.0),
+ (0.38, 0.77, 0.38, 1.0),
+ (0.38, 0.77, 0.38, 1.0)
+ ),
+ "yellow": (
+ (0.0, 0.0, 0.0, 1.0),
+ (0.77, 0.77, 0.16, 1.0),
+ (0.77, 0.77, 0.16, 1.0)
+ ),
+ "purple": (
+ (0.0, 0.0, 0.0, 1.0),
+ (0.38, 0.38, 0.77, 1.0),
+ (0.38, 0.38, 0.77, 1.0)
+ ),
+ "grey": (
+ (0.0, 0.0, 0.0, 1.0),
+ (0.63, 0.63, 0.63, 1.0),
+ (0.63, 0.63, 0.63, 1.0)
+ ),
+ "black": (
+ (1.0, 1.0, 1.0, 0.7),
+ (0.0, 0.0, 0.0, 0.7),
+ (0.2, 0.2, 0.2, 1.0)
+ )
+}
+
+
+def nice_hotkey_name(punc):
+ # convert the ugly string name into the actual character
+ pairs = (
+ ('LEFTMOUSE', "LMB"),
+ ('MIDDLEMOUSE', "MMB"),
+ ('RIGHTMOUSE', "RMB"),
+ ('SELECTMOUSE', "Select"),
+ ('WHEELUPMOUSE', "Wheel Up"),
+ ('WHEELDOWNMOUSE', "Wheel Down"),
+ ('WHEELINMOUSE', "Wheel In"),
+ ('WHEELOUTMOUSE', "Wheel Out"),
+ ('ZERO', "0"),
+ ('ONE', "1"),
+ ('TWO', "2"),
+ ('THREE', "3"),
+ ('FOUR', "4"),
+ ('FIVE', "5"),
+ ('SIX', "6"),
+ ('SEVEN', "7"),
+ ('EIGHT', "8"),
+ ('NINE', "9"),
+ ('OSKEY', "Super"),
+ ('RET', "Enter"),
+ ('LINE_FEED', "Enter"),
+ ('SEMI_COLON', ";"),
+ ('PERIOD', "."),
+ ('COMMA', ","),
+ ('QUOTE', '"'),
+ ('MINUS', "-"),
+ ('SLASH', "/"),
+ ('BACK_SLASH', "\\"),
+ ('EQUAL', "="),
+ ('NUMPAD_1', "Numpad 1"),
+ ('NUMPAD_2', "Numpad 2"),
+ ('NUMPAD_3', "Numpad 3"),
+ ('NUMPAD_4', "Numpad 4"),
+ ('NUMPAD_5', "Numpad 5"),
+ ('NUMPAD_6', "Numpad 6"),
+ ('NUMPAD_7', "Numpad 7"),
+ ('NUMPAD_8', "Numpad 8"),
+ ('NUMPAD_9', "Numpad 9"),
+ ('NUMPAD_0', "Numpad 0"),
+ ('NUMPAD_PERIOD', "Numpad ."),
+ ('NUMPAD_SLASH', "Numpad /"),
+ ('NUMPAD_ASTERIX', "Numpad *"),
+ ('NUMPAD_MINUS', "Numpad -"),
+ ('NUMPAD_ENTER', "Numpad Enter"),
+ ('NUMPAD_PLUS', "Numpad +"),
+ )
+ nice_punc = False
+ for (ugly, nice) in pairs:
+ if punc == ugly:
+ nice_punc = nice
+ break
+ if not nice_punc:
+ nice_punc = punc.replace("_", " ").title()
+ return nice_punc
+
+
+def hack_force_update(context, nodes):
+ if context.space_data.tree_type == "ShaderNodeTree":
+ node = nodes.new('ShaderNodeMath')
+ node.inputs[0].default_value = 0.0
+ nodes.remove(node)
+ elif context.space_data.tree_type == "CompositorNodeTree":
+ node = nodes.new('CompositorNodeMath')
+ node.inputs[0].default_value = 0.0
+ nodes.remove(node)
+ return False
+
+
+def dpifac():
+ return bpy.context.user_preferences.system.dpi/72
+
+
+def is_end_node(node):
+ bool = True
+ for output in node.outputs:
+ if output.links:
+ bool = False
+ break
+ return bool
+
+
+def node_mid_pt(node, axis):
+ if axis == 'x':
+ d = node.location.x + (node.dimensions.x / 2)
+ elif axis == 'y':
+ d = node.location.y - (node.dimensions.y / 2)
+ else:
+ d = 0
+ return d
+
+
+def autolink(node1, node2, links):
+ link_made = False
+
+ for outp in node1.outputs:
+ for inp in node2.inputs:
+ if not inp.is_linked and inp.name == outp.name:
+ link_made = True
+ links.new(outp, inp)
+ return True
+
+ for outp in node1.outputs:
+ for inp in node2.inputs:
+ if not inp.is_linked and inp.type == outp.type:
+ link_made = True
+ links.new(outp, inp)
+ return True
+
+ # force some connection even if the type doesn't match
+ for outp in node1.outputs:
+ for inp in node2.inputs:
+ if not inp.is_linked:
+ link_made = True
+ links.new(outp, inp)
+ return True
+
+ # even if no sockets are open, force one of matching type
+ for outp in node1.outputs:
+ for inp in node2.inputs:
+ if inp.type == outp.type:
+ link_made = True
+ links.new(outp, inp)
+ return True
+
+ # do something!
+ for outp in node1.outputs:
+ for inp in node2.inputs:
+ link_made = True
+ links.new(outp, inp)
+ return True
+
+ print("Could not make a link from " + node1.name + " to " + node2.name)
+ return link_made
+
+
+def node_at_pos(nodes, context, event):
+ nodes_near_mouse = []
+ nodes_under_mouse = []
+ target_node = None
+
+ store_mouse_cursor(context, event)
+ x, y = context.space_data.cursor_location
+ x = x
+ y = y
+
+ # Make a list of each corner (and middle of border) for each node.
+ # Will be sorted to find nearest point and thus nearest node
+ node_points_with_dist = []
+ for node in nodes:
+ skipnode = False
+ if node.type != 'FRAME': # no point trying to link to a frame node
+ locx = node.location.x
+ locy = node.location.y
+ dimx = node.dimensions.x/dpifac()
+ dimy = node.dimensions.y/dpifac()
+ if node.parent:
+ locx += node.parent.location.x
+ locy += node.parent.location.y
+ if node.parent.parent:
+ locx += node.parent.parent.location.x
+ locy += node.parent.parent.location.y
+ if node.parent.parent.parent:
+ locx += node.parent.parent.parent.location.x
+ locy += node.parent.parent.parent.location.y
+ if node.parent.parent.parent.parent:
+ # Support three levels or parenting
+ # There's got to be a better way to do this...
+ skipnode = True
+ if not skipnode:
+ node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
+ node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
+ node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
+ node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
+
+ node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
+ node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
+ node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
+ node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
+
+ nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
+
+ for node in nodes:
+ if node.type != 'FRAME' and skipnode == False:
+ locx = node.location.x
+ locy = node.location.y
+ dimx = node.dimensions.x/dpifac()
+ dimy = node.dimensions.y/dpifac()
+ if node.parent:
+ locx += node.parent.location.x
+ locy += node.parent.location.y
+ if (locx <= x <= locx + dimx) and \
+ (locy - dimy <= y <= locy):
+ nodes_under_mouse.append(node)
+
+ if len(nodes_under_mouse) == 1:
+ if nodes_under_mouse[0] != nearest_node:
+ target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
+ else:
+ target_node = nearest_node # else use the nearest node
+ else:
+ target_node = nearest_node
+ return target_node
+
+
+def store_mouse_cursor(context, event):
+ space = context.space_data
+ v2d = context.region.view2d
+ tree = space.edit_tree
+
+ # convert mouse position to the View2D for later node placement
+ if context.region.type == 'WINDOW':
+ space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
+ else:
+ space.cursor_location = tree.view_center
+
+
+def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]):
+ bgl.glEnable(bgl.GL_BLEND)
+ bgl.glLineWidth(size)
+ bgl.glShadeModel(bgl.GL_SMOOTH)
+
+ bgl.glBegin(bgl.GL_LINE_STRIP)
+ try:
+ bgl.glColor4f(colour[0]+(1.0-colour[0])/4, colour[1]+(1.0-colour[1])/4, colour[2]+(1.0-colour[2])/4, colour[3]+(1.0-colour[3])/4)
+ bgl.glVertex2f(x1, y1)
+ bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
+ bgl.glVertex2f(x2, y2)
+ except:
+ pass
+ bgl.glEnd()
+ bgl.glShadeModel(bgl.GL_FLAT)
+
+
+def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]):
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
+ radius = radius
+ sides = 32
+ for i in range(sides + 1):
+ cosine = radius * cos(i * 2 * pi / sides) + mx
+ sine = radius * sin(i * 2 * pi / sides) + my
+ bgl.glVertex2f(cosine, sine)
+ bgl.glEnd()
+
+
+def draw_rounded_node_border(node, radius=8, colour=[1.0, 1.0, 1.0, 0.7]):
+ bgl.glEnable(bgl.GL_BLEND)
+ settings = bpy.context.user_preferences.addons[__name__].preferences
+ if settings.bgl_antialiasing:
+ bgl.glEnable(bgl.GL_LINE_SMOOTH)
+
+ area_width = bpy.context.area.width - (16*dpifac()) - 1
+ bottom_bar = (16*dpifac()) + 1
+ sides = 16
+ radius = radius*dpifac()
+ bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
+
+ nlocx = (node.location.x+1)*dpifac()
+ nlocy = (node.location.y+1)*dpifac()
+ ndimx = node.dimensions.x
+ ndimy = node.dimensions.y
+ # This is a stupid way to do this... TODO use while loop
+ if node.parent:
+ nlocx += node.parent.location.x
+ nlocy += node.parent.location.y
+ if node.parent.parent:
+ nlocx += node.parent.parent.location.x
+ nlocy += node.parent.parent.location.y
+ if node.parent.parent.parent:
+ nlocx += node.parent.parent.parent.location.x
+ nlocy += node.parent.parent.parent.location.y
+
+ if node.hide:
+ nlocx += -1
+ nlocy += 5
+ if node.type == 'REROUTE':
+ #nlocx += 1
+ nlocy -= 1
+ ndimx = 0
+ ndimy = 0
+ radius += 6
+
+ # Top left corner
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
+ bgl.glVertex2f(mx,my)
+ for i in range(sides+1):
+ if (4<=i<=8):
+ if my > bottom_bar and mx < area_width:
+ cosine = radius * cos(i * 2 * pi / sides) + mx
+ sine = radius * sin(i * 2 * pi / sides) + my
+ bgl.glVertex2f(cosine, sine)
+ bgl.glEnd()
+
+ # Top right corner
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
+ bgl.glVertex2f(mx,my)
+ for i in range(sides+1):
+ if (0<=i<=4):
+ if my > bottom_bar and mx < area_width:
+ cosine = radius * cos(i * 2 * pi / sides) + mx
+ sine = radius * sin(i * 2 * pi / sides) + my
+ bgl.glVertex2f(cosine, sine)
+ bgl.glEnd()
+
+ # Bottom left corner
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
+ bgl.glVertex2f(mx,my)
+ for i in range(sides+1):
+ if (8<=i<=12):
+ if my > bottom_bar and mx < area_width:
+ cosine = radius * cos(i * 2 * pi / sides) + mx
+ sine = radius * sin(i * 2 * pi / sides) + my
+ bgl.glVertex2f(cosine, sine)
+ bgl.glEnd()
+
+ # Bottom right corner
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
+ bgl.glVertex2f(mx,my)
+ for i in range(sides+1):
+ if (12<=i<=16):
+ if my > bottom_bar and mx < area_width:
+ cosine = radius * cos(i * 2 * pi / sides) + mx
+ sine = radius * sin(i * 2 * pi / sides) + my
+ bgl.glVertex2f(cosine, sine)
+ bgl.glEnd()
+
+
+ # Left edge
+ bgl.glBegin(bgl.GL_QUADS)
+ m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
+ m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
+ m1y = max(m1y, bottom_bar)
+ m2y = max(m2y, bottom_bar)
+ if m1x < area_width and m2x < area_width:
+ bgl.glVertex2f(m2x-radius,m2y) # draw order is important, start with bottom left and go anti-clockwise
+ bgl.glVertex2f(m2x,m2y)
+ bgl.glVertex2f(m1x,m1y)
+ bgl.glVertex2f(m1x-radius,m1y)
+ bgl.glEnd()
+
+ # Top edge
+ bgl.glBegin(bgl.GL_QUADS)
+ m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
+ m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
+ m1x = min(m1x, area_width)
+ m2x = min(m2x, area_width)
+ if m1y > bottom_bar and m2y > bottom_bar:
+ bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
+ bgl.glVertex2f(m2x,m2y)
+ bgl.glVertex2f(m2x,m1y+radius)
+ bgl.glVertex2f(m1x,m1y+radius)
+ bgl.glEnd()
+
+ # Right edge
+ bgl.glBegin(bgl.GL_QUADS)
+ m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
+ m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
+ m1y = max(m1y, bottom_bar)
+ m2y = max(m2y, bottom_bar)
+ if m1x < area_width and m2x < area_width:
+ bgl.glVertex2f(m2x,m2y) # draw order is important, start with bottom left and go anti-clockwise
+ bgl.glVertex2f(m2x+radius,m2y)
+ bgl.glVertex2f(m1x+radius,m1y)
+ bgl.glVertex2f(m1x,m1y)
+ bgl.glEnd()
+
+ # Bottom edge
+ bgl.glBegin(bgl.GL_QUADS)
+ m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
+ m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
+ m1x = min(m1x, area_width)
+ m2x = min(m2x, area_width)
+ if m1y > bottom_bar and m2y > bottom_bar:
+ bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
+ bgl.glVertex2f(m2x,m2y)
+ bgl.glVertex2f(m2x,m1y-radius)
+ bgl.glVertex2f(m1x,m1y-radius)
+ bgl.glEnd()
+
+
+ # Restore defaults
+ bgl.glDisable(bgl.GL_BLEND)
+ if settings.bgl_antialiasing:
+ bgl.glDisable(bgl.GL_LINE_SMOOTH)
+
+
+def draw_callback_mixnodes(self, context, mode):
+ if self.mouse_path:
+ nodes = context.space_data.node_tree.nodes
+ settings = context.user_preferences.addons[__name__].preferences
+ if settings.bgl_antialiasing:
+ bgl.glEnable(bgl.GL_LINE_SMOOTH)
+
+ if mode == "LINK":
+ col_outer = [1.0, 0.2, 0.2, 0.4]
+ col_inner = [0.0, 0.0, 0.0, 0.5]
+ col_circle_inner = [0.3, 0.05, 0.05, 1.0]
+ if mode == "LINKMENU":
+ col_outer = [0.4, 0.6, 1.0, 0.4]
+ col_inner = [0.0, 0.0, 0.0, 0.5]
+ col_circle_inner = [0.08, 0.15, .3, 1.0]
+ elif mode == "MIX":
+ col_outer = [0.2, 1.0, 0.2, 0.4]
+ col_inner = [0.0, 0.0, 0.0, 0.5]
+ col_circle_inner = [0.05, 0.3, 0.05, 1.0]
+
+ m1x = self.mouse_path[0][0]
+ m1y = self.mouse_path[0][1]
+ m2x = self.mouse_path[-1][0]
+ m2y = self.mouse_path[-1][1]
+
+ n1 = nodes[context.scene.NWLazySource]
+ n2 = nodes[context.scene.NWLazyTarget]
+
+ if n1 != n2:
+ draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
+ draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
+ draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
+ draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
+
+ draw_line(m1x, m1y, m2x, m2y, 4, col_outer) # line outline
+ draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
+
+ # circle outline
+ draw_circle(m1x, m1y, 6, col_outer)
+ draw_circle(m2x, m2y, 6, col_outer)
+
+ # circle inner
+ draw_circle(m1x, m1y, 5, col_circle_inner)
+ draw_circle(m2x, m2y, 5, col_circle_inner)
+
+ # restore opengl defaults
+ bgl.glLineWidth(1)
+ bgl.glDisable(bgl.GL_BLEND)
+ bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
+
+ if settings.bgl_antialiasing:
+ bgl.glDisable(bgl.GL_LINE_SMOOTH)
+
+
+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
+
+
+# Addon prefs
+class NWNodeWrangler(bpy.types.AddonPreferences):
+ bl_idname = __name__
+
+ merge_hide = EnumProperty(
+ name="Hide Mix nodes",
+ items=(
+ ("ALWAYS", "Always", "Always collapse the new merge nodes"),
+ ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
+ ("NEVER", "Never", "Never collapse the new merge nodes")
+ ),
+ default='NON_SHADER',
+ description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded")
+ merge_position = EnumProperty(
+ name="Mix Node Position",
+ items=(
+ ("CENTER", "Center", "Place the Mix node between the two nodes"),
+ ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
+ ),
+ default='CENTER',
+ description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes")
+ bgl_antialiasing = BoolProperty(
+ name="Line Antialiasing",
+ default=False,
+ description="Remove aliasing artifacts on lines drawn in interactive modes such as Lazy Connect (Alt+LMB) and Lazy Merge (Alt+RMB) - this may cause issues on some systems"
+ )
+
+ show_hotkey_list = BoolProperty(
+ name="Show Hotkey List",
+ default=False,
+ description="Expand this box into a list of all the hotkeys for functions in this addon"
+ )
+ hotkey_list_filter = StringProperty(
+ name=" Filter by Name",
+ default="",
+ description="Show only hotkeys that have this text in their name"
+ )
+
+ def draw(self, context):
+ layout = self.layout
+ col = layout.column()
+ col.prop(self, "merge_position")
+ col.prop(self, "merge_hide")
+ col.prop(self, "bgl_antialiasing")
+
+ box = col.box()
+ col = box.column(align=True)
+
+ hotkey_button_name = "Show Hotkey List"
+ if self.show_hotkey_list:
+ hotkey_button_name = "Hide Hotkey List"
+ col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
+ if self.show_hotkey_list:
+ col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
+ col.separator()
+ for hotkey in kmi_defs:
+ if hotkey[7]:
+ hotkey_name = hotkey[7]
+
+ if self.hotkey_list_filter.lower() in hotkey_name.lower():
+ row = col.row(align=True)
+ row.label(hotkey_name)
+ keystr = nice_hotkey_name(hotkey[1])
+ if hotkey[4]:
+ keystr = "Shift " + keystr
+ if hotkey[5]:
+ keystr = "Alt " + keystr
+ if hotkey[3]:
+ keystr = "Ctrl " + keystr
+ row.label(keystr)
+
+def nw_check(context):
+ space = context.space_data
+ valid = False
+ if space.type == 'NODE_EDITOR' and space.node_tree is not None:
+ valid = True
+
+ return valid
+
+class NWBase:
+ @classmethod
+ def poll(cls, context):
+ return nw_check(context)
+
+
+# OPERATORS
+class NWLazyMix(Operator, NWBase):
+ """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
+ bl_idname = "node.nw_lazy_mix"
+ bl_label = "Mix Nodes"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def modal(self, context, event):
+ context.area.tag_redraw()
+ nodes, links = get_nodes_links(context)
+ cont = True
+
+ start_pos = [event.mouse_region_x, event.mouse_region_y]
+
+ node1 = None
+ if not context.scene.NWBusyDrawing:
+ node1 = node_at_pos(nodes, context, event)
+ if node1:
+ context.scene.NWBusyDrawing = node1.name
+ else:
+ if context.scene.NWBusyDrawing != 'STOP':
+ node1 = nodes[context.scene.NWBusyDrawing]
+
+ context.scene.NWLazySource = node1.name
+ context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
+
+ if event.type == 'MOUSEMOVE':
+ self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
+
+ elif event.type == 'RIGHTMOUSE':
+ end_pos = [event.mouse_region_x, event.mouse_region_y]
+ bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
+
+ node2 = None
+ node2 = node_at_pos(nodes, context, event)
+ if node2:
+ context.scene.NWBusyDrawing = node2.name
+
+ if node1 == node2:
+ cont = False
+
+ if cont:
+ if node1 and node2:
+ for node in nodes:
+ node.select = False
+ node1.select = True
+ node2.select = True
+
+ bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
+
+ context.scene.NWBusyDrawing = ""
+ return {'FINISHED'}
+
+ elif event.type == 'ESC':
+ print('cancelled')
+ bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
+ return {'CANCELLED'}
+
+ return {'RUNNING_MODAL'}
+
+ def invoke(self, context, event):
+ if context.area.type == 'NODE_EDITOR':
+ # the arguments we pass the the callback
+ args = (self, context, 'MIX')
+ # Add the region OpenGL drawing callback
+ # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
+ self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
+
+ self.mouse_path = []
+
+ context.window_manager.modal_handler_add(self)
+ return {'RUNNING_MODAL'}
+ else:
+ self.report({'WARNING'}, "View3D not found, cannot run operator")
+ return {'CANCELLED'}
+
+
+class NWLazyConnect(Operator, NWBase):
+ """Connect two nodes without clicking a specific socket (automatically determined"""
+ bl_idname = "node.nw_lazy_connect"
+ bl_label = "Lazy Connect"
+ bl_options = {'REGISTER', 'UNDO'}
+ with_menu = BoolProperty()
+
+ def modal(self, context, event):
+ context.area.tag_redraw()
+ nodes, links = get_nodes_links(context)
+ cont = True
+
+ start_pos = [event.mouse_region_x, event.mouse_region_y]
+
+ node1 = None
+ if not context.scene.NWBusyDrawing:
+ node1 = node_at_pos(nodes, context, event)
+ if node1:
+ context.scene.NWBusyDrawing = node1.name
+ else:
+ if context.scene.NWBusyDrawing != 'STOP':
+ node1 = nodes[context.scene.NWBusyDrawing]
+
+ context.scene.NWLazySource = node1.name
+ context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
+
+ if event.type == 'MOUSEMOVE':
+ self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
+
+ elif event.type == 'RIGHTMOUSE':
+ end_pos = [event.mouse_region_x, event.mouse_region_y]
+ bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
+
+ node2 = None
+ node2 = node_at_pos(nodes, context, event)
+ if node2:
+ context.scene.NWBusyDrawing = node2.name
+
+ if node1 == node2:
+ cont = False
+
+ link_success = False
+ if cont:
+ if node1 and node2:
+ original_sel = []
+ original_unsel = []
+ for node in nodes:
+ if node.select == True:
+ node.select = False
+ original_sel.append(node)
+ else:
+ original_unsel.append(node)
+ node1.select = True
+ node2.select = True
+
+ #link_success = autolink(node1, node2, links)
+ if self.with_menu:
+ if len(node1.outputs) > 1 and node2.inputs:
+ bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
+ elif len(node1.outputs) == 1:
+ bpy.ops.node.nw_call_inputs_menu(from_socket=0)
+ else:
+ link_success = autolink(node1, node2, links)
+
+ for node in original_sel:
+ node.select = True
+ for node in original_unsel:
+ node.select = False
+
+ if link_success:
+ hack_force_update(context, nodes)
+ context.scene.NWBusyDrawing = ""
+ return {'FINISHED'}
+
+ elif event.type == 'ESC':
+ bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
+ return {'CANCELLED'}
+
+ return {'RUNNING_MODAL'}
+
+ def invoke(self, context, event):
+ if context.area.type == 'NODE_EDITOR':
+ nodes, links = get_nodes_links(context)
+ node = node_at_pos(nodes, context, event)
+ if node:
+ context.scene.NWBusyDrawing = node.name
+
+ # the arguments we pass the the callback
+ mode = "LINK"
+ if self.with_menu:
+ mode = "LINKMENU"
+ args = (self, context, mode)
+ # Add the region OpenGL drawing callback
+ # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
+ self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_mixnodes, args, 'WINDOW', 'POST_PIXEL')
+
+ self.mouse_path = []
+
+ context.window_manager.modal_handler_add(self)
+ return {'RUNNING_MODAL'}
+ else:
+ self.report({'WARNING'}, "View3D not found, cannot run operator")
+ return {'CANCELLED'}
+
+
+class NWDeleteUnused(Operator, NWBase):
+ """Delete all nodes whose output is not used"""
+ bl_idname = 'node.nw_del_unused'
+ bl_label = 'Delete Unused Nodes'
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ valid = False
+ if nw_check(context):
+ if context.space_data.node_tree.nodes:
+ valid = True
+ return valid
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ end_types = 'OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
+ 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LAMP', \
+ 'OUTPUT_WORLD', 'GROUP', 'GROUP_INPUT', 'GROUP_OUTPUT'
+
+ # Store selection
+ selection = []
+ for node in nodes:
+ if node.select == True:
+ selection.append(node.name)
+
+ deleted_nodes = []
+ temp_deleted_nodes = []
+ del_unused_iterations = len(nodes)
+ for it in range(0, del_unused_iterations):
+ temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
+ for node in nodes:
+ node.select = False
+ for node in nodes:
+ if is_end_node(node) and not node.type in end_types and node.type != 'FRAME':
+ node.select = True
+ deleted_nodes.append(node.name)
+ bpy.ops.node.delete()
+
+ if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
+ break
+ # get unique list of deleted nodes (iterations would count the same node more than once)
+ deleted_nodes = list(set(deleted_nodes))
+ for n in deleted_nodes:
+ self.report({'INFO'}, "Node " + n + " deleted")
+ num_deleted = len(deleted_nodes)
+ n = ' node'
+ if num_deleted > 1:
+ n += 's'
+ if num_deleted:
+ self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
+ else:
+ self.report({'INFO'}, "Nothing deleted")
+
+ # Restore selection
+ nodes, links = get_nodes_links(context)
+ for node in nodes:
+ if node.name in selection:
+ node.select = True
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_confirm(self, event)
+
+
+class NWSwapLinks(Operator, NWBase):
+ """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
+ bl_idname = 'node.nw_swap_links'
+ bl_label = 'Swap Links'
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ valid = False
+ if nw_check(context):
+ if context.selected_nodes:
+ valid = len(context.selected_nodes) <= 2
+ return valid
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ selected_nodes = context.selected_nodes
+ n1 = selected_nodes[0]
+
+ # Swap outputs
+ if len(selected_nodes) == 2:
+ n2 = selected_nodes[1]
+ if n1.outputs and n2.outputs:
+ n1_outputs = []
+ n2_outputs = []
+
+ out_index = 0
+ for output in n1.outputs:
+ if output.links:
+ for link in output.links:
+ n1_outputs.append([out_index, link.to_socket])
+ links.remove(link)
+ out_index += 1
+
+ out_index = 0
+ for output in n2.outputs:
+ if output.links:
+ for link in output.links:
+ n2_outputs.append([out_index, link.to_socket])
+ links.remove(link)
+ out_index += 1
+
+ for connection in n1_outputs:
+ try:
+ links.new(n2.outputs[connection[0]], connection[1])
+ except:
+ self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
+ for connection in n2_outputs:
+ try:
+ links.new(n1.outputs[connection[0]], connection[1])
+ except:
+ self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
+ else:
+ if n1.outputs or n2.outputs:
+ self.report({'WARNING'}, "One of the nodes has no outputs!")
+ else:
+ self.report({'WARNING'}, "Neither of the nodes have outputs!")
+
+ # Swap Inputs
+ elif len(selected_nodes) == 1:
+ if n1.inputs:
+ types = []
+ i=0
+ for i1 in n1.inputs:
+ if i1.is_linked:
+ similar_types = 0
+ for i2 in n1.inputs:
+ if i1.type == i2.type and i2.is_linked:
+ similar_types += 1
+ types.append ([i1, similar_types, i])
+ i += 1
+ types.sort(key=lambda k: k[1], reverse=True)
+
+ if types:
+ t = types[0]
+ if t[1] == 2:
+ for i2 in n1.inputs:
+ if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
+ pair = [t[0], i2]
+ i1f = pair[0].links[0].from_socket
+ i1t = pair[0].links[0].to_socket
+ i2f = pair[1].links[0].from_socket
+ i2t = pair[1].links[0].to_socket
+ links.new(i1f, i2t)
+ links.new(i2f, i1t)
+ if t[1] == 1:
+ if len(types) == 1:
+ fs = t[0].links[0].from_socket
+ i = t[2]
+ links.remove(t[0].links[0])
+ if i+1 == len(n1.inputs):
+ i = -1
+ i += 1
+ while n1.inputs[i].is_linked:
+ i += 1
+ links.new(fs, n1.inputs[i])
+ elif len(types) == 2:
+ i1f = types[0][0].links[0].from_socket
+ i1t = types[0][0].links[0].to_socket
+ i2f = types[1][0].links[0].from_socket
+ i2t = types[1][0].links[0].to_socket
+ links.new(i1f, i2t)
+ links.new(i2f, i1t)
+
+ else:
+ self.report({'WARNING'}, "This node has no input connections to swap!")
+ else:
+ self.report({'WARNING'}, "This node has no inputs to swap!")
+
+ hack_force_update(context, nodes)
+ return {'FINISHED'}
+
+
+class NWResetBG(Operator, NWBase):
+ """Reset the zoom and position of the background image"""
+ bl_idname = 'node.nw_bg_reset'
+ bl_label = 'Reset Backdrop'
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ valid = False
+ if nw_check(context):
+ snode = context.space_data
+ valid = snode.tree_type == 'CompositorNodeTree'
+ return valid
+
+ def execute(self, context):
+ context.space_data.backdrop_zoom = 1
+ context.space_data.backdrop_x = 0
+ context.space_data.backdrop_y = 0
+ return {'FINISHED'}
+
+
+class NWAddAttrNode(Operator, NWBase):
+ """Add an Attribute node with this name"""
+ bl_idname = 'node.nw_add_attr_node'
+ bl_label = 'Add UV map'
+ attr_name = StringProperty()
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
+ nodes, links = get_nodes_links(context)
+ nodes.active.attribute_name = self.attr_name
+ return {'FINISHED'}
+
+
+class NWEmissionViewer(Operator, NWBase):
+ bl_idname = "node.nw_emission_viewer"
+ bl_label = "Emission Viewer"
+ bl_description = "Connect active node to Emission Shader for shadeless previews"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ is_cycles = context.scene.render.engine == 'CYCLES'
+ valid = False
+ if nw_check(context):
+ space = context.space_data
+ if space.tree_type == 'ShaderNodeTree' and is_cycles and\
+ (context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD"):
+ valid = True
+ return valid
+
+ def invoke(self, context, event):
+ shader_type = context.space_data.shader_type
+ if shader_type == 'OBJECT':
+ shader_output_type = "OUTPUT_MATERIAL"
+ shader_output_ident = "ShaderNodeOutputMaterial"
+ shader_viewer_ident = "ShaderNodeEmission"
+ elif shader_type == 'WORLD':
+ shader_output_type = "OUTPUT_WORLD"
+ shader_output_ident = "ShaderNodeOutputWorld"
+ shader_viewer_ident = "ShaderNodeBackground"
+ shader_types = [x[1] for x in shaders_shader_nodes_props]
+ mlocx = event.mouse_region_x
+ mlocy = event.mouse_region_y
+ select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
+ if 'FINISHED' in select_node: # only run if mouse click is on a node
+ nodes, links = get_nodes_links(context)
+ in_group = context.active_node != context.space_data.node_tree.nodes.active
+ active = nodes.active
+ output_types = [x[1] for x in shaders_output_nodes_props]
+ valid = False
+ if active:
+ if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
+ for out in active.outputs:
+ if not out.hide:
+ valid = True
+ break
+ if valid:
+ # get material_output node, store selection, deselect all
+ materialout_exists = False
+ materialout = None # placeholder node
+ selection = []
+ for node in nodes:
+ if node.type == shader_output_type:
+ materialout_exists = True
+ materialout = node
+ if node.select:
+ selection.append(node.name)
+ node.select = False
+ if not materialout:
+ materialout = nodes.new(shader_output_ident)
+ sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
+ max_xloc_node = sorted_by_xloc[-1]
+ if max_xloc_node.name == 'Emission Viewer':
+ max_xloc_node = sorted_by_xloc[-2]
+ materialout.location.x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
+ sum_yloc = 0
+ for node in nodes:
+ sum_yloc += node.location.y
+ # put material output at average y location
+ materialout.location.y = sum_yloc / len(nodes)
+ materialout.select = False
+ # Analyze outputs, add "Emission Viewer" if needed, make links
+ out_i = None
+ valid_outputs = []
+ for i, out in enumerate(active.outputs):
+ if not out.hide:
+ valid_outputs.append(i)
+ if valid_outputs:
+ out_i = valid_outputs[0] # Start index of node's outputs
+ for i, valid_i in enumerate(valid_outputs):
+ for out_link in active.outputs[valid_i].links:
+ linked_to_out = False
+ if "Emission Viewer" in out_link.to_node.name or out_link.to_node == materialout:
+ linked_to_out = True
+ if linked_to_out:
+ if i < len(valid_outputs) - 1:
+ out_i = valid_outputs[i + 1]
+ else:
+ out_i = valid_outputs[0]
+ make_links = [] # store sockets for new links
+ if active.outputs:
+ # If output type not 'SHADER' - "Emission Viewer" needed
+ if active.outputs[out_i].type != 'SHADER':
+ # get Emission Viewer node
+ emission_exists = False
+ emission_placeholder = nodes[0]
+ for node in nodes:
+ if "Emission Viewer" in node.name:
+ emission_exists = True
+ emission_placeholder = node
+ if not emission_exists:
+ emission = nodes.new(shader_viewer_ident)
+ emission.hide = True
+ emission.location = [materialout.location.x, (materialout.location.y + 40)]
+ emission.label = "Viewer"
+ emission.name = "Emission Viewer"
+ emission.use_custom_color = True
+ emission.color = (0.6, 0.5, 0.4)
+ emission.select = False
+ else:
+ emission = emission_placeholder
+ make_links.append((active.outputs[out_i], emission.inputs[0]))
+ make_links.append((emission.outputs[0], materialout.inputs[0]))
+ else:
+ # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
+ make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
+ for node in nodes:
+ if node.name == 'Emission Viewer':
+ node.select = True
+ bpy.ops.node.delete()
+ for li_from, li_to in make_links:
+ links.new(li_from, li_to)
+ # Restore selection
+ nodes.active = active
+ for node in nodes:
+ if node.name in selection:
+ node.select = True
+ hack_force_update(context, nodes)
+ return {'FINISHED'}
+ else:
+ return {'CANCELLED'}
+
+
+class NWFrameSelected(Operator, NWBase):
+ bl_idname = "node.nw_frame_selected"
+ bl_label = "Frame Selected"
+ bl_description = "Add a frame node and parent the selected nodes to it"
+ bl_options = {'REGISTER', 'UNDO'}
+ label_prop = StringProperty(name='Label', default=' ', description='The visual name of the frame node')
+ color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6),
+ min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3)
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ selected = []
+ for node in nodes:
+ if node.select == True:
+ selected.append(node)
+
+ bpy.ops.node.add_node(type='NodeFrame')
+ frm = nodes.active
+ frm.label = self.label_prop
+ frm.use_custom_color = True
+ frm.color = self.color_prop
+
+ for node in selected:
+ node.parent = frm
+
+ return {'FINISHED'}
+
+
+class NWReloadImages(Operator, NWBase):
+ bl_idname = "node.nw_reload_images"
+ bl_label = "Reload Images"
+ bl_description = "Update all the image nodes to match their files on disk"
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
+ num_reloaded = 0
+ for node in nodes:
+ if node.type in image_types:
+ if node.type == "TEXTURE":
+ if node.texture: # node has texture assigned
+ if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
+ if node.texture.image: # texture has image assigned
+ node.texture.image.reload()
+ num_reloaded += 1
+ else:
+ if node.image:
+ node.image.reload()
+ num_reloaded += 1
+
+ if num_reloaded:
+ self.report({'INFO'}, "Reloaded images")
+ print("Reloaded " + str(num_reloaded) + " images")
+ hack_force_update(context, nodes)
+ return {'FINISHED'}
+ else:
+ self.report({'WARNING'}, "No images found to reload in this node tree")
+ return {'CANCELLED'}
+
+
+class NWSwitchNodeType(Operator, NWBase):
+ """Switch type of selected nodes """
+ bl_idname = "node.nw_swtch_node_type"
+ bl_label = "Switch Node Type"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ to_type = EnumProperty(
+ name="Switch to type",
+ items=list(shaders_input_nodes_props) +
+ list(shaders_output_nodes_props) +
+ list(shaders_shader_nodes_props) +
+ list(shaders_texture_nodes_props) +
+ list(shaders_color_nodes_props) +
+ list(shaders_vector_nodes_props) +
+ list(shaders_converter_nodes_props) +
+ list(shaders_layout_nodes_props) +
+ list(compo_input_nodes_props) +
+ list(compo_output_nodes_props) +
+ list(compo_color_nodes_props) +
+ list(compo_converter_nodes_props) +
+ list(compo_filter_nodes_props) +
+ list(compo_vector_nodes_props) +
+ list(compo_matte_nodes_props) +
+ list(compo_distort_nodes_props) +
+ list(compo_layout_nodes_props) +
+ list(blender_mat_input_nodes_props) +
+ list(blender_mat_output_nodes_props) +
+ list(blender_mat_color_nodes_props) +
+ list(blender_mat_vector_nodes_props) +
+ list(blender_mat_converter_nodes_props) +
+ list(blender_mat_layout_nodes_props) +
+ list(texture_input_nodes_props) +
+ list(texture_output_nodes_props) +
+ list(texture_color_nodes_props) +
+ list(texture_pattern_nodes_props) +
+ list(texture_textures_nodes_props) +
+ list(texture_converter_nodes_props) +
+ list(texture_distort_nodes_props) +
+ list(texture_layout_nodes_props)
+ )
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ to_type = self.to_type
+ # Those types of nodes will not swap.
+ src_excludes = ('NodeFrame')
+ # Those attributes of nodes will be copied if possible
+ attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
+ 'show_options', 'show_preview', 'show_texture',
+ 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
+ )
+ selected = [n for n in nodes if n.select]
+ reselect = []
+ for node in [n for n in selected if
+ n.rna_type.identifier not in src_excludes and
+ n.rna_type.identifier != to_type]:
+ new_node = nodes.new(to_type)
+ for attr in attrs_to_pass:
+ if hasattr(node, attr) and hasattr(new_node, attr):
+ setattr(new_node, attr, getattr(node, attr))
+ # set image datablock of dst to image of src
+ if hasattr(node, 'image') and hasattr(new_node, 'image'):
+ if node.image:
+ new_node.image = node.image
+ # Special cases
+ if new_node.type == 'SWITCH':
+ new_node.hide = True
+ # Dictionaries: src_sockets and dst_sockets:
+ # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
+ # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
+ # in 'INPUTS' and 'OUTPUTS':
+ # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
+ # socket entry:
+ # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
+ src_sockets = {
+ 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
+ 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
+ }
+ dst_sockets = {
+ 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
+ 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
+ }
+ types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
+ types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
+ # check src node to set src_sockets values and dst node to set dst_sockets dict values
+ for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
+ # Check node's inputs and outputs and fill proper entries in "sockets" dict
+ for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
+ # enumerate in inputs, then in outputs
+ # find name, default value and links of socket
+ for i, socket in enumerate(in_out):
+ the_name = socket.name
+ dval = None
+ # Not every socket, especially in outputs has "default_value"
+ if hasattr(socket, 'default_value'):
+ dval = socket.default_value
+ socket_links = []
+ for lnk in socket.links:
+ socket_links.append(lnk)
+ # check type of socket to fill proper keys.
+ for the_type in types_order_one:
+ if socket.type == the_type:
+ # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
+ # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
+ sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
+ # Check which of the types in inputs/outputs is considered to be "main".
+ # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
+ for type_check in types_order_one:
+ if sockets[in_out_name][type_check]:
+ sockets[in_out_name]['MAIN'] = type_check
+ break
+
+ matches = {
+ 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
+ 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
+ }
+
+ for inout, soctype in (
+ ('INPUTS', 'MAIN',),
+ ('INPUTS', 'SHADER',),
+ ('INPUTS', 'RGBA',),
+ ('INPUTS', 'VECTOR',),
+ ('INPUTS', 'VALUE',),
+ ('OUTPUTS', 'MAIN',),
+ ('OUTPUTS', 'SHADER',),
+ ('OUTPUTS', 'RGBA',),
+ ('OUTPUTS', 'VECTOR',),
+ ('OUTPUTS', 'VALUE',),
+ ):
+ if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
+ if soctype == 'MAIN':
+ sc = src_sockets[inout][src_sockets[inout]['MAIN']]
+ dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
+ else:
+ sc = src_sockets[inout][soctype]
+ dt = dst_sockets[inout][soctype]
+ # start with 'dt' to determine number of possibilities.
+ for i, soc in enumerate(dt):
+ # if src main has enough entries - match them with dst main sockets by indexes.
+ if len(sc) > i:
+ matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
+ # add 'VALUE_NAME' criterion to inputs.
+ if inout == 'INPUTS' and soctype == 'VALUE':
+ for s in sc:
+ if s[2] == soc[2]: # if names match
+ # append src (index, dval), dst (index, dval)
+ matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
+
+ # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
+ # This creates better links when relinking textures.
+ if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
+ matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
+
+ # Pass default values and RELINK:
+ for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
+ # INPUTS: Base on matches in proper order.
+ for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
+ # pass dvals
+ if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
+ new_node.inputs[dst_i].default_value = src_dval
+ # Special case: switch to math
+ if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
+ new_node.type == 'MATH' and\
+ tp == 'MAIN':
+ new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
+ new_node.inputs[dst_i].default_value = new_dst_dval
+ if node.type == 'MIX_RGB':
+ if node.blend_type in [o[0] for o in operations]:
+ new_node.operation = node.blend_type
+ # Special case: switch from math to some types
+ if node.type == 'MATH' and\
+ new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
+ tp == 'MAIN':
+ for i in range(3):
+ new_node.inputs[dst_i].default_value[i] = src_dval
+ if new_node.type == 'MIX_RGB':
+ if node.operation in [t[0] for t in blend_types]:
+ new_node.blend_type = node.operation
+ # Set Fac of MIX_RGB to 1.0
+ new_node.inputs[0].default_value = 1.0
+ # make link only when dst matching input is not linked already.
+ if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
+ in_src_link = node.inputs[src_i].links[0]
+ in_dst_socket = new_node.inputs[dst_i]
+ links.new(in_src_link.from_socket, in_dst_socket)
+ links.remove(in_src_link)
+ # OUTPUTS: Base on matches in proper order.
+ for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
+ for out_src_link in node.outputs[src_i].links:
+ out_dst_socket = new_node.outputs[dst_i]
+ links.new(out_dst_socket, out_src_link.to_socket)
+ # relink rest inputs if possible, no criteria
+ for src_inp in node.inputs:
+ for dst_inp in new_node.inputs:
+ if src_inp.links and not dst_inp.links:
+ src_link = src_inp.links[0]
+ links.new(src_link.from_socket, dst_inp)
+ links.remove(src_link)
+ # relink rest outputs if possible, base on node kind if any left.
+ for src_o in node.outputs:
+ for out_src_link in src_o.links:
+ for dst_o in new_node.outputs:
+ if src_o.type == dst_o.type:
+ links.new(dst_o, out_src_link.to_socket)
+ # relink rest outputs no criteria if any left. Link all from first output.
+ for src_o in node.outputs:
+ for out_src_link in src_o.links:
+ if new_node.outputs:
+ links.new(new_node.outputs[0], out_src_link.to_socket)
+ nodes.remove(node)
+ return {'FINISHED'}
+
+
+class NWMergeNodes(Operator, NWBase):
+ bl_idname = "node.nw_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'),
+ ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
+ ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
+ ),
+ )
+
+ def execute(self, context):
+ settings = context.user_preferences.addons[__name__].preferences
+ merge_hide = settings.merge_hide
+ merge_position = settings.merge_position # 'center' or 'bottom'
+
+ do_hide = False
+ do_hide_shader = False
+ if merge_hide == 'ALWAYS':
+ do_hide = True
+ do_hide_shader = True
+ elif merge_hide == 'NON_SHADER':
+ do_hide = True
+
+ tree_type = context.space_data.node_tree.type
+ if tree_type == 'COMPOSITING':
+ node_type = 'CompositorNode'
+ elif tree_type == 'SHADER':
+ node_type = 'ShaderNode'
+ elif tree_type == 'TEXTURE':
+ node_type = 'TextureNode'
+ nodes, links = get_nodes_links(context)
+ mode = self.mode
+ merge_type = self.merge_type
+ # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
+ # 'ZCOMBINE' works only if mode == 'MIX'
+ # Setting mode to None prevents trying to add 'ZCOMBINE' node.
+ if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
+ merge_type = 'MIX'
+ mode = 'MIX'
+ selected_mix = [] # entry = [index, loc]
+ selected_shader = [] # entry = [index, loc]
+ selected_math = [] # entry = [index, loc]
+ selected_z = [] # entry = [index, loc]
+ selected_alphaover = [] # 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', ('MIX', 'ADD'), 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, node.dimensions.x, node.hide])
+ else:
+ for (type, types_list, dst) in (
+ ('SHADER', ('MIX', 'ADD'), selected_shader),
+ ('MIX', [t[0] for t in blend_types], selected_mix),
+ ('MATH', [t[0] for t in operations], selected_math),
+ ('ZCOMBINE', ('MIX', ), selected_z),
+ ('ALPHAOVER', ('MIX', ), selected_alphaover),
+ ):
+ if merge_type == type and mode in types_list:
+ dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
+ # 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, selected_z, selected_alphaover]:
+ 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] + nodes_list[0][3] + 70
+ nodes_list.sort(key=lambda k: k[2], reverse=True)
+ if merge_position == 'CENTER':
+ loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2 # average yloc of last two nodes (lowest two)
+ if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
+ if do_hide:
+ loc_y += 40
+ else:
+ loc_y += 80
+ else:
+ loc_y = nodes_list[len(nodes_list) - 1][2]
+ offset_y = 100
+ if not do_hide:
+ offset_y = 200
+ if nodes_list == selected_shader and not do_hide_shader:
+ offset_y = 150.0
+ the_range = len(nodes_list) - 1
+ if len(nodes_list) == 1:
+ the_range = 1
+ 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
+ if mode != 'MIX':
+ add.inputs[0].default_value = 1.0
+ add.show_preview = False
+ add.hide = do_hide
+ if do_hide:
+ loc_y = loc_y - 50
+ 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
+ if do_hide:
+ loc_y = loc_y - 50
+ 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)
+ add.hide = do_hide_shader
+ if do_hide_shader:
+ loc_y = loc_y - 50
+ first = 1
+ second = 2
+ add.width_hidden = 100.0
+ elif mode == 'ADD':
+ add_type = node_type + 'AddShader'
+ add = nodes.new(add_type)
+ add.hide = do_hide_shader
+ if do_hide_shader:
+ loc_y = loc_y - 50
+ first = 0
+ second = 1
+ add.width_hidden = 100.0
+ elif nodes_list == selected_z:
+ add = nodes.new('CompositorNodeZcombine')
+ add.show_preview = False
+ add.hide = do_hide
+ if do_hide:
+ loc_y = loc_y - 50
+ first = 0
+ second = 2
+ add.width_hidden = 100.0
+ elif nodes_list == selected_alphaover:
+ add = nodes.new('CompositorNodeAlphaOver')
+ add.show_preview = False
+ add.hide = do_hide
+ if do_hide:
+ loc_y = loc_y - 50
+ first = 1
+ second = 2
+ add.width_hidden = 100.0
+ add.location = loc_x, loc_y
+ loc_y += offset_y
+ add.select = True
+ count_adds = i + 1
+ count_after = len(nodes)
+ index = count_after - 1
+ first_selected = nodes[nodes_list[0][0]]
+ # "last" node has been added as first, so its index is count_before.
+ last_add = nodes[count_before]
+ # Special case:
+ # Two nodes were selected and first selected has no output links, second selected has output links.
+ # Then add links from last add to all links 'to_socket' of out links of second selected.
+ if len(nodes_list) == 2:
+ if not first_selected.outputs[0].links:
+ second_selected = nodes[nodes_list[1][0]]
+ for ss_link in second_selected.outputs[0].links:
+ # Prevent cyclic dependencies when nodes to be marged are linked to one another.
+ # Create list of invalid indexes.
+ invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
+ # Link only if "to_node" index not in invalid indexes list.
+ if ss_link.to_node not in [nodes[i] for i in invalid_i]:
+ links.new(last_add.outputs[0], ss_link.to_socket)
+ # add links from last_add to all links 'to_socket' of out links of first selected.
+ for fs_link in first_selected.outputs[0].links:
+ # Prevent cyclic dependencies when nodes to be marged are linked to one another.
+ # Create list of invalid indexes.
+ invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
+ # Link only if "to_node" index not in invalid indexes list.
+ if fs_link.to_node not in [nodes[i] for i in invalid_i]:
+ links.new(last_add.outputs[0], fs_link.to_socket)
+ # add link from "first" selected and "first" add node
+ node_to = nodes[count_after - 1]
+ links.new(first_selected.outputs[0], node_to.inputs[first])
+ if node_to.type == 'ZCOMBINE':
+ for fs_out in first_selected.outputs:
+ if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
+ links.new(fs_out, node_to.inputs[1])
+ break
+ # add links between added ADD nodes and between selected and ADD nodes
+ for i in range(count_adds):
+ if i < count_adds - 1:
+ node_from = nodes[index]
+ node_to = nodes[index - 1]
+ node_to_input_i = first
+ node_to_z_i = 1 # if z combine - link z to first z input
+ links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
+ if node_to.type == 'ZCOMBINE':
+ for from_out in node_from.outputs:
+ if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
+ links.new(from_out, node_to.inputs[node_to_z_i])
+ if len(nodes_list) > 1:
+ node_from = nodes[nodes_list[i + 1][0]]
+ node_to = nodes[index]
+ node_to_input_i = second
+ node_to_z_i = 3 # if z combine - link z to second z input
+ links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
+ if node_to.type == 'ZCOMBINE':
+ for from_out in node_from.outputs:
+ if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
+ links.new(from_out, node_to.inputs[node_to_z_i])
+ index -= 1
+ # set "last" of added nodes as active
+ nodes.active = last_add
+ for i, x, y, dx, h in nodes_list:
+ nodes[i].select = False
+
+ return {'FINISHED'}
+
+
+class NWBatchChangeNodes(Operator, NWBase):
+ bl_idname = "node.nw_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 NWChangeMixFactor(Operator, NWBase):
+ bl_idname = "node.nw_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 NWCopySettings(Operator, NWBase):
+ bl_idname = "node.nw_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):
+ valid = False
+ if nw_check(context):
+ if 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 NWCopyLabel(Operator, NWBase):
+ bl_idname = "node.nw_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 NWClearLabel(Operator, NWBase):
+ bl_idname = "node.nw_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 NWModifyLabels(Operator, NWBase):
+ """Modify Labels of all selected nodes"""
+ bl_idname = "node.nw_modify_labels"
+ bl_label = "Modify Labels"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ prepend = StringProperty(
+ name="Add to Beginning"
+ )
+ append = StringProperty(
+ name="Add to End"
+ )
+ replace_from = StringProperty(
+ name="Text to Replace"
+ )
+ replace_to = StringProperty(
+ name="Replace with"
+ )
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ for node in [n for n in nodes if n.select]:
+ node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
+
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ self.prepend = ""
+ self.append = ""
+ self.remove = ""
+ return context.window_manager.invoke_props_dialog(self)
+
+
+class NWAddTextureSetup(Operator, NWBase):
+ bl_idname = "node.nw_add_texture"
+ bl_label = "Texture Setup"
+ bl_description = "Add Texture Node Setup to Selected Shaders"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ valid = False
+ if nw_check(context):
+ space = context.space_data
+ if space.tree_type == 'ShaderNodeTree' and context.scene.render.engine == 'CYCLES':
+ valid = True
+ return valid
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ active = nodes.active
+ shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
+ texture_types = [x[1] for x in shaders_texture_nodes_props]
+ valid = False
+ if active:
+ if active.select:
+ if active.type in shader_types or active.type in texture_types:
+ if not active.inputs[0].is_linked:
+ valid = True
+ if valid:
+ locx = active.location.x
+ locy = active.location.y
+
+ xoffset = [500.0, 700.0]
+ isshader = True
+ if active.type not in shader_types:
+ xoffset = [290.0, 500.0]
+ isshader = False
+
+ coordout = 2
+ image_type = 'ShaderNodeTexImage'
+
+ if (active.type in texture_types and active.type != 'TEX_IMAGE') or (active.type == 'BACKGROUND'):
+ coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
+ if active.type == 'BACKGROUND':
+ image_type = 'ShaderNodeTexEnvironment'
+
+ if isshader:
+ tex = nodes.new(image_type)
+ tex.location = [locx - 200.0, locy + 28.0]
+
+ map = nodes.new('ShaderNodeMapping')
+ map.location = [locx - xoffset[0], locy + 80.0]
+ map.width = 240
+ coord = nodes.new('ShaderNodeTexCoord')
+ coord.location = [locx - xoffset[1], locy + 40.0]
+ active.select = False
+
+ if isshader:
+ nodes.active = tex
+ links.new(tex.outputs[0], active.inputs[0])
+ links.new(map.outputs[0], tex.inputs[0])
+ links.new(coord.outputs[coordout], map.inputs[0])
+
+ else:
+ nodes.active = map
+ links.new(map.outputs[0], active.inputs[0])
+ links.new(coord.outputs[coordout], map.inputs[0])
+
+ return {'FINISHED'}
+
+
+class NWAddReroutes(Operator, NWBase):
+ """Add Reroute Nodes and link them to outputs of selected nodes"""
+ bl_idname = "node.nw_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 NWLinkActiveToSelected(Operator, NWBase):
+ """Link active node to selected nodes basing on various criteria"""
+ bl_idname = "node.nw_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):
+ valid = False
+ if nw_check(context):
+ if 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 NWAlignNodes(Operator, NWBase):
+ bl_idname = "node.nw_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):
+ 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
+ if node.select:
+ if node.type == 'FRAME':
+ node.select = False
+ frames_reselect.append(i)
+ else:
+ locx = node.location.x
+ locy = node.location.y
+ width = node.dimensions[0]
+ height = node.dimensions[1]
+ 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
+ # calculate relative locations
+ parent = node.parent
+ while parent is not None:
+ locx += parent.location.x
+ locy += parent.location.y
+ parent = parent.parent
+ selected.append([i, locx, locy, width, height])
+ count = len(selected)
+ if count > 1: # aligning makes sense only if at least 2 nodes are selected
+ selected_sorted_x = sorted(selected, key=lambda k: (k[1], -k[2]))
+ selected_sorted_y = sorted(selected, key=lambda k: (-k[2], k[1]))
+ min_x = selected_sorted_x[0][1] # min loc.x
+ min_x_loc_y = selected_sorted_x[0][2] # loc y of node with min loc x
+ min_x_w = selected_sorted_x[0][3] # width of node with max loc x
+ max_x = selected_sorted_x[count - 1][1] # max loc.x
+ max_x_loc_y = selected_sorted_x[count - 1][2] # loc y of node with max loc.x
+ max_x_w = selected_sorted_x[count - 1][3] # width of node with max loc.x
+ min_y = selected_sorted_y[0][2] # min loc.y
+ min_y_loc_x = selected_sorted_y[0][1] # loc.x of node with min loc.y
+ min_y_h = selected_sorted_y[0][4] # height of node with min loc.y
+ min_y_w = selected_sorted_y[0][3] # width of node with min loc.y
+ max_y = selected_sorted_y[count - 1][2] # max loc.y
+ max_y_loc_x = selected_sorted_y[count - 1][1] # loc x of node with max loc.y
+ max_y_w = selected_sorted_y[count - 1][3] # width of node with max loc.y
+ max_y_h = selected_sorted_y[count - 1][4] # height of node with max loc.y
+
+ if self.option == 'AXIS_Y': # Horizontally. Equivelent of s -> x -> 0 with even spacing.
+ loc_x = min_x
+ #loc_y = (max_x_loc_y + min_x_loc_y) / 2.0
+ loc_y = (max_y - max_y_h / 2.0 + min_y - min_y_h / 2.0) / 2.0
+ offset_x = (max_x - min_x - total_w + max_x_w) / (count - 1)
+ for i, x, y, w, h in selected_sorted_x:
+ nodes[i].location.x = loc_x
+ nodes[i].location.y = loc_y + h / 2.0
+ parent = nodes[i].parent
+ while parent is not None:
+ nodes[i].location.x -= parent.location.x
+ nodes[i].location.y -= parent.location.y
+ parent = parent.parent
+ loc_x += offset_x + w
+ else: # if self.option == 'AXIS_Y'
+ loc_x = (max_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 NWSelectParentChildren(Operator, NWBase):
+ bl_idname = "node.nw_select_parent_child"
+ bl_label = "Select Parent or Children"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ option = EnumProperty(
+ name="option",
+ items=(
+ ('PARENT', 'Select Parent', 'Select Parent Frame'),
+ ('CHILD', 'Select Children', 'Select members of selected frame'),
+ )
+ )
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ option = self.option
+ selected = [node for node in nodes if node.select]
+ if option == 'PARENT':
+ for sel in selected:
+ parent = sel.parent
+ if parent:
+ parent.select = True
+ else: # option == 'CHILD'
+ for sel in selected:
+ children = [node for node in nodes if node.parent == sel]
+ for kid in children:
+ kid.select = True
+
+ return {'FINISHED'}
+
+
+class NWDetachOutputs(Operator, NWBase):
+ """Detach outputs of selected node leaving inluts liked"""
+ bl_idname = "node.nw_detach_outputs"
+ bl_label = "Detach Outputs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ selected = context.selected_nodes
+ bpy.ops.node.duplicate_move_keep_inputs()
+ new_nodes = context.selected_nodes
+ bpy.ops.node.select_all(action="DESELECT")
+ for node in selected:
+ node.select = True
+ bpy.ops.node.delete_reconnect()
+ for new_node in new_nodes:
+ new_node.select = True
+ bpy.ops.transform.translate('INVOKE_DEFAULT')
+
+ return {'FINISHED'}
+
+
+class NWLinkToOutputNode(Operator, NWBase):
+ """Link to Composite node or Material Output node"""
+ bl_idname = "node.nw_link_out"
+ bl_label = "Connect to Output"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ valid = False
+ if nw_check(context):
+ if context.active_node is not None:
+ for out in context.active_node.outputs:
+ if not out.hide:
+ valid = True
+ break
+ return valid
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ active = nodes.active
+ output_node = None
+ output_index = None
+ tree_type = context.space_data.tree_type
+ output_types_shaders = [x[1] for x in shaders_output_nodes_props]
+ output_types_compo = ['COMPOSITE']
+ output_types_blender_mat = ['OUTPUT']
+ output_types_textures = ['OUTPUT']
+ output_types = output_types_shaders + output_types_compo + output_types_blender_mat
+ for node in nodes:
+ if node.type in output_types:
+ output_node = node
+ break
+ if not output_node:
+ bpy.ops.node.select_all(action="DESELECT")
+ if tree_type == 'ShaderNodeTree':
+ if context.scene.render.engine == 'CYCLES':
+ output_node = nodes.new('ShaderNodeOutputMaterial')
+ else:
+ output_node = nodes.new('ShaderNodeOutput')
+ elif tree_type == 'CompositorNodeTree':
+ output_node = nodes.new('CompositorNodeComposite')
+ elif tree_type == 'TextureNodeTree':
+ output_node = nodes.new('TextureNodeOutput')
+ output_node.location.x = active.location.x + active.dimensions.x + 80
+ output_node.location.y = active.location.y
+ if (output_node and active.outputs):
+ for i, output in enumerate(active.outputs):
+ if not output.hide:
+ output_index = i
+ break
+ for i, output in enumerate(active.outputs):
+ if output.type == output_node.inputs[0].type and not output.hide:
+ output_index = i
+ break
+
+ out_input_index = 0
+ if tree_type == 'ShaderNodeTree' and context.scene.render.engine == 'CYCLES':
+ if active.outputs[output_index].name == 'Volume':
+ out_input_index = 1
+ elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
+ out_input_index = 2
+ links.new(active.outputs[output_index], output_node.inputs[out_input_index])
+
+ hack_force_update(context, nodes) # viewport render does not update
+
+ return {'FINISHED'}
+
+
+class NWMakeLink(Operator, NWBase):
+ """Make a link from one socket to another"""
+ bl_idname = 'node.nw_make_link'
+ bl_label = 'Make Link'
+ bl_options = {'REGISTER', 'UNDO'}
+ from_socket = IntProperty()
+ to_socket = IntProperty()
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+
+ n1 = nodes[context.scene.NWLazySource]
+ n2 = nodes[context.scene.NWLazyTarget]
+
+ links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
+
+ hack_force_update(context, nodes)
+
+ return {'FINISHED'}
+
+
+class NWCallInputsMenu(Operator, NWBase):
+ """Link from this output"""
+ bl_idname = 'node.nw_call_inputs_menu'
+ bl_label = 'Make Link'
+ bl_options = {'REGISTER', 'UNDO'}
+ from_socket = IntProperty()
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+
+ context.scene.NWSourceSocket = self.from_socket
+
+ n1 = nodes[context.scene.NWLazySource]
+ n2 = nodes[context.scene.NWLazyTarget]
+ if len(n2.inputs) > 1:
+ bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
+ elif len(n2.inputs) == 1:
+ links.new(n1.outputs[self.from_socket], n2.inputs[0])
+ return {'FINISHED'}
+
+
+class NWAddSequence(Operator, ImportHelper):
+ """Add an Image Sequence"""
+ bl_idname = 'node.nw_add_sequence'
+ bl_label = 'Import Image Sequence'
+ bl_options = {'REGISTER', 'UNDO'}
+ directory = StringProperty(subtype="DIR_PATH")
+ filename = StringProperty(subtype="FILE_NAME")
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ directory = self.directory
+ filename = self.filename
+
+
+ if context.space_data.node_tree.type == 'SHADER':
+ node_type = "ShaderNodeTexImage"
+ elif context.space_data.node_tree.type == 'COMPOSITING':
+ node_type = "CompositorNodeImage"
+ else:
+ self.report({'ERROR'}, "Unsupported Node Tree type!")
+ return {'CANCELLED'}
+
+ without_ext = '.'.join(filename.split('.')[:-1])
+
+ # if last digit isn't a number, it's not a sequence
+ if without_ext[-1].isdigit():
+ without_ext = without_ext[:-1] + '1'
+ else:
+ self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
+ return {'CANCELLED'}
+
+
+ extension = filename.split('.')[-1]
+ reverse = without_ext[::-1] # reverse string
+
+ count_numbers = 0
+ for char in reverse:
+ if char.isdigit():
+ count_numbers += 1
+ else:
+ break
+
+ without_num = without_ext[:count_numbers*-1]
+
+ files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
+
+ num_frames = len(files)
+
+ nodes_list = [node for node in nodes]
+ if nodes_list:
+ nodes_list.sort(key=lambda k: k.location.x)
+ xloc = nodes_list[0].location.x - 220 # place new nodes at far left
+ yloc = 0
+ for node in nodes:
+ node.select = False
+ yloc += node_mid_pt(node, 'y')
+ yloc = yloc/len(nodes)
+ else:
+ xloc = 0
+ yloc = 0
+
+ name_with_hashes = without_num + "#"*count_numbers + '.' + extension
+
+ node = nodes.new(node_type)
+ node.location.x = xloc
+ node.location.y = yloc + 110
+ node.label = name_with_hashes
+
+ img = bpy.data.images.load(directory+(without_ext+'.'+extension))
+ img.source = 'SEQUENCE'
+ img.name = name_with_hashes
+ node.image = img
+ node.frame_offset = int(files[0][len(without_num)+len(directory):-1*(len(extension)+1)]) - 1 # separate the number from the file name of the first file
+ if context.space_data.node_tree.type == 'SHADER':
+ node.image_user.frame_duration = num_frames
+ else:
+ node.frame_duration = num_frames
+
+ return {'FINISHED'}
+
+
+class NWAddMultipleImages(Operator, ImportHelper):
+ """Add multiple images at once"""
+ bl_idname = 'node.nw_add_multiple_images'
+ bl_label = 'Open Selected Images'
+ bl_options = {'REGISTER', 'UNDO'}
+ directory = StringProperty(subtype="DIR_PATH")
+ files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
+
+ def execute(self, context):
+ nodes, links = get_nodes_links(context)
+ nodes_list = [node for node in nodes]
+ if nodes_list:
+ nodes_list.sort(key=lambda k: k.location.x)
+ xloc = nodes_list[0].location.x - 220 # place new nodes at far left
+ yloc = 0
+ for node in nodes:
+ node.select = False
+ yloc += node_mid_pt(node, 'y')
+ yloc = yloc/len(nodes)
+ else:
+ xloc = 0
+ yloc = 0
+
+ if context.space_data.node_tree.type == 'SHADER':
+ node_type = "ShaderNodeTexImage"
+ elif context.space_data.node_tree.type == 'COMPOSITING':
+ node_type = "CompositorNodeImage"
+ else:
+ self.report({'ERROR'}, "Unsupported Node Tree type!")
+ return {'CANCELLED'}
+
+ new_nodes = []
+ for f in self.files:
+ fname = f.name
+
+ node = nodes.new(node_type)
+ new_nodes.append(node)
+ node.label = fname
+ node.hide = True
+ node.width_hidden = 100
+ node.location.x = xloc
+ node.location.y = yloc
+ yloc -= 40
+
+ img = bpy.data.images.load(self.directory+fname)
+ node.image = img
+
+ # shift new nodes up to center of tree
+ list_size = new_nodes[0].location.y - new_nodes[-1].location.y
+ for node in new_nodes:
+ node.select = True
+ node.location.y += (list_size/2)
+ return {'FINISHED'}
+
+
+class NWViewerFocus(bpy.types.Operator):
+ """Set the viewer tile center to the mouse position"""
+ bl_idname = "node.nw_viewer_focus"
+ bl_label = "Viewer Focus"
+
+ x = bpy.props.IntProperty()
+ y = bpy.props.IntProperty()
+
+ @classmethod
+ def poll(cls, context):
+ return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
+
+ def execute(self, context):
+ return {'FINISHED'}
+
+ def invoke(self, context, event):
+ render = context.scene.render
+ space = context.space_data
+ percent = render.resolution_percentage*0.01
+
+ for n in bpy.context.scene.node_tree.nodes:
+ if n.type == "VIEWER":
+ self.x = event.mouse_region_x
+ self.y = event.mouse_region_y
+
+ region_x=context.region.width
+ region_y=context.region.height
+
+ region_center_x=region_x/2
+ region_center_y=region_y/2
+
+ rel_region_mouse_x=region_x-self.x
+ rel_region_mouse_y=region_y-self.y
+
+ bd_x = render.resolution_x*percent*space.backdrop_zoom
+ bd_y = render.resolution_y* percent*space.backdrop_zoom
+
+ backdrop_center_x=(bd_x/2)-space.backdrop_x
+ backdrop_center_y=(bd_y/2)-space.backdrop_y
+
+ margin_x = region_center_x-backdrop_center_x
+ margin_y = region_center_y-backdrop_center_y
+
+ absolute_x_max = margin_x+bd_x
+ absolute_y_max = margin_y+bd_y
+
+ abs_mouse_x = (self.x-margin_x)/bd_x
+ abs_mouse_y = (self.y-margin_y)/bd_y
+
+ rel_bd_x = (bd_x-rel_region_mouse_x)
+ rel_bd_y = (bd_y-rel_region_mouse_y)
+
+ n.center_x = abs_mouse_x
+ n.center_y = abs_mouse_y
+
+ return self.execute(context)
+
+
+#
+# P A N E L
+#
+
+def drawlayout(context, layout, mode='non-panel'):
+ tree_type = context.space_data.tree_type
+
+ col = layout.column(align=True)
+ col.menu(NWMergeNodesMenu.bl_idname)
+ col.separator()
+
+ col = layout.column(align=True)
+ col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
+ col.separator()
+
+ if tree_type == 'ShaderNodeTree' and context.scene.render.engine == 'CYCLES':
+ col = layout.column(align=True)
+ col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
+ col.separator()
+
+ col = layout.column(align=True)
+ col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
+ col.operator(NWSwapLinks.bl_idname)
+ col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
+ col.separator()
+
+ col = layout.column(align=True)
+ col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
+ col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
+ col.separator()
+
+ col = layout.column(align=True)
+ if mode == 'panel':
+ row = col.row(align=True)
+ row.operator(NWClearLabel.bl_idname).option = True
+ row.operator(NWModifyLabels.bl_idname)
+ else:
+ col.operator(NWClearLabel.bl_idname).option = True
+ col.operator(NWModifyLabels.bl_idname)
+ col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
+ col.separator()
+ col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
+ col.separator()
+
+ col = layout.column(align=True)
+ if tree_type == 'CompositorNodeTree':
+ col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
+ col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
+ col.separator()
+
+ col = layout.column(align=True)
+ col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
+ col.separator()
+
+ col = layout.column(align=True)
+ col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
+ col.separator()
+
+
+class NodeWranglerPanel(Panel, NWBase):
+ bl_idname = "NODE_PT_nw_node_wrangler"
+ bl_space_type = 'NODE_EDITOR'
+ bl_region_type = 'UI'
+ bl_label = "Node Wrangler"
+
+ prepend = StringProperty(
+ name='prepend',
+ )
+ append = StringProperty()
+ remove = StringProperty()
+
+ def draw(self, context):
+ self.layout.label(text="(Quick access: Ctrl+Space)")
+ drawlayout(context, self.layout, mode='panel')
+
+
+#
+# M E N U S
+#
+class NodeWranglerMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_node_wrangler_menu"
+ bl_label = "Node Wrangler"
+
+ def draw(self, context):
+ drawlayout(context, self.layout)
+
+
+class NWMergeNodesMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_merge_nodes_menu"
+ bl_label = "Merge Selected Nodes"
+
+ def draw(self, context):
+ type = context.space_data.tree_type
+ layout = self.layout
+ if type == 'ShaderNodeTree' and context.scene.render.engine == 'CYCLES':
+ layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
+ layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
+ layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
+ props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
+ props.mode = 'MIX'
+ props.merge_type = 'ZCOMBINE'
+ props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
+ props.mode = 'MIX'
+ props.merge_type = 'ALPHAOVER'
+
+
+class NWMergeShadersMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_merge_shaders_menu"
+ bl_label = "Merge Selected Nodes using Shaders"
+
+ def draw(self, context):
+ layout = self.layout
+ for type in ('MIX', 'ADD'):
+ props = layout.operator(NWMergeNodes.bl_idname, text=type)
+ props.mode = type
+ props.merge_type = 'SHADER'
+
+
+class NWMergeMixMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_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(NWMergeNodes.bl_idname, text=name)
+ props.mode = type
+ props.merge_type = 'MIX'
+
+
+class NWConnectionListOutputs(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_connection_list_out"
+ bl_label = "From:"
+
+ def draw(self, context):
+ layout = self.layout
+ nodes, links = get_nodes_links(context)
+
+ n1 = nodes[context.scene.NWLazySource]
+
+ if n1.type == "R_LAYERS":
+ index=0
+ for o in n1.outputs:
+ if o.enabled: # Check which passes the render layer has enabled
+ layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
+ index+=1
+ else:
+ index=0
+ for o in n1.outputs:
+ layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
+ index+=1
+
+
+class NWConnectionListInputs(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_connection_list_in"
+ bl_label = "To:"
+
+ def draw(self, context):
+ layout = self.layout
+ nodes, links = get_nodes_links(context)
+
+ n2 = nodes[context.scene.NWLazyTarget]
+
+ index = 0
+ for i in n2.inputs:
+ op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
+ op.from_socket = context.scene.NWSourceSocket
+ op.to_socket = index
+ index+=1
+
+
+class NWMergeMathMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_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(NWMergeNodes.bl_idname, text=name)
+ props.mode = type
+ props.merge_type = 'MATH'
+
+
+class NWBatchChangeNodesMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
+ bl_label = "Batch Change Selected Nodes"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
+ layout.menu(NWBatchChangeOperationMenu.bl_idname)
+
+
+class NWBatchChangeBlendTypeMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_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(NWBatchChangeNodes.bl_idname, text=name)
+ props.blend_type = type
+ props.operation = 'CURRENT'
+
+
+class NWBatchChangeOperationMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_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(NWBatchChangeNodes.bl_idname, text=name)
+ props.blend_type = 'CURRENT'
+ props.operation = type
+
+
+class NWCopyToSelectedMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_copy_node_properties_menu"
+ bl_label = "Copy to Selected"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
+ layout.menu(NWCopyLabelMenu.bl_idname)
+
+
+class NWCopyLabelMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_copy_label_menu"
+ bl_label = "Copy Label"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
+ layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
+ layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
+
+
+class NWAddReroutesMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_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(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
+ layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
+ layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
+
+
+class NWLinkActiveToSelectedMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
+ bl_label = "Link Active to Selected"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.menu(NWLinkStandardMenu.bl_idname)
+ layout.menu(NWLinkUseNodeNameMenu.bl_idname)
+ layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
+
+
+class NWLinkStandardMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_link_standard_menu"
+ bl_label = "To All Selected"
+
+ def draw(self, context):
+ layout = self.layout
+ props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
+ props.replace = False
+ props.use_node_name = False
+ props.use_outputs_names = False
+ props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
+ props.replace = True
+ props.use_node_name = False
+ props.use_outputs_names = False
+
+
+class NWLinkUseNodeNameMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_link_use_node_name_menu"
+ bl_label = "Use Node Name/Label"
+
+ def draw(self, context):
+ layout = self.layout
+ props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
+ props.replace = False
+ props.use_node_name = True
+ props.use_outputs_names = False
+ props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
+ props.replace = True
+ props.use_node_name = True
+ props.use_outputs_names = False
+
+
+class NWLinkUseOutputsNamesMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
+ bl_label = "Use Outputs Names"
+
+ def draw(self, context):
+ layout = self.layout
+ props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
+ props.replace = False
+ props.use_node_name = False
+ props.use_outputs_names = True
+ props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
+ props.replace = True
+ props.use_node_name = False
+ props.use_outputs_names = True
+
+
+class NWNodeAlignMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_node_align_menu"
+ bl_label = "Align Nodes"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator(NWAlignNodes.bl_idname, text="Horizontally").option = 'AXIS_X'
+ layout.operator(NWAlignNodes.bl_idname, text="Vertically").option = 'AXIS_Y'
+
+
+class NWVertColMenu(bpy.types.Menu):
+ bl_idname = "NODE_MT_nw_node_vertex_color_menu"
+ bl_label = "Vertex Colors"
+
+ @classmethod
+ def poll(cls, context):
+ valid = False
+ if nw_check(context):
+ snode = context.space_data
+ valid = snode.tree_type == 'ShaderNodeTree' and context.scene.render.engine == 'CYCLES'
+ return valid
+
+ def draw(self, context):
+ l = self.layout
+ nodes, links = get_nodes_links(context)
+ mat = context.object.active_material
+
+ objs = []
+ for obj in bpy.data.objects:
+ for slot in obj.material_slots:
+ if slot.material == mat:
+ objs.append(obj)
+ vcols = []
+ for obj in objs:
+ if obj.data.vertex_colors:
+ for vcol in obj.data.vertex_colors:
+ vcols.append(vcol.name)
+ vcols = list(set(vcols)) # get a unique list
+
+ if vcols:
+ for vcol in vcols:
+ l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
+ else:
+ l.label("No Vertex Color layers on objects with this material")
+
+
+class NWSwitchNodeTypeMenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_node_type_menu"
+ bl_label = "Switch Type to..."
+
+ def draw(self, context):
+ layout = self.layout
+ tree = context.space_data.node_tree
+ if tree.type == 'SHADER':
+ if context.scene.render.engine == 'CYCLES':
+ layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
+ layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
+ layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
+ layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
+ layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
+ layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
+ layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
+ layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
+ if context.scene.render.engine != 'CYCLES':
+ layout.menu(NWSwitchMatInputSubmenu.bl_idname)
+ layout.menu(NWSwitchMatOutputSubmenu.bl_idname)
+ layout.menu(NWSwitchMatColorSubmenu.bl_idname)
+ layout.menu(NWSwitchMatVectorSubmenu.bl_idname)
+ layout.menu(NWSwitchMatConverterSubmenu.bl_idname)
+ layout.menu(NWSwitchMatLayoutSubmenu.bl_idname)
+ if tree.type == 'COMPOSITING':
+ layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
+ layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
+ if tree.type == 'TEXTURE':
+ layout.menu(NWSwitchTexInputSubmenu.bl_idname)
+ layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
+ layout.menu(NWSwitchTexColorSubmenu.bl_idname)
+ layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
+ layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
+ layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
+ layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
+ layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
+
+
+class NWSwitchShadersInputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
+ bl_label = "Input"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_input_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchShadersOutputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
+ bl_label = "Output"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_output_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchShadersShaderSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
+ bl_label = "Shader"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_shader_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchShadersTextureSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
+ bl_label = "Texture"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_texture_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchShadersColorSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
+ bl_label = "Color"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_color_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchShadersVectorSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
+ bl_label = "Vector"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_vector_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchShadersConverterSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
+ bl_label = "Converter"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_converter_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
+ bl_label = "Layout"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in shaders_layout_nodes_props:
+ if type != 'FRAME':
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoInputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
+ bl_label = "Input"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_input_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoOutputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
+ bl_label = "Output"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_output_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoColorSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
+ bl_label = "Color"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_color_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoConverterSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
+ bl_label = "Converter"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_converter_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoFilterSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
+ bl_label = "Filter"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_filter_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoVectorSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
+ bl_label = "Vector"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_vector_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoMatteSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
+ bl_label = "Matte"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_matte_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoDistortSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
+ bl_label = "Distort"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_distort_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
+ bl_label = "Layout"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in compo_layout_nodes_props:
+ if type != 'FRAME':
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchMatInputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
+ bl_label = "Input"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in blender_mat_input_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchMatOutputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
+ bl_label = "Output"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in blender_mat_output_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchMatColorSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
+ bl_label = "Color"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in blender_mat_color_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchMatVectorSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
+ bl_label = "Vector"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in blender_mat_vector_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchMatConverterSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
+ bl_label = "Converter"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in blender_mat_converter_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchMatLayoutSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
+ bl_label = "Layout"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in blender_mat_layout_nodes_props:
+ if type != 'FRAME':
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexInputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
+ bl_label = "Input"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_input_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexOutputSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
+ bl_label = "Output"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_output_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexColorSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
+ bl_label = "Color"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_color_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexPatternSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
+ bl_label = "Pattern"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_pattern_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexTexturesSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
+ bl_label = "Textures"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_textures_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexConverterSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
+ bl_label = "Converter"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_converter_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexDistortSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
+ bl_label = "Distort"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_distort_nodes_props:
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+class NWSwitchTexLayoutSubmenu(Menu, NWBase):
+ bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
+ bl_label = "Layout"
+
+ def draw(self, context):
+ layout = self.layout
+ for ident, type, rna_name in texture_layout_nodes_props:
+ if type != 'FRAME':
+ props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
+ props.to_type = ident
+
+
+#
+# APPENDAGES TO EXISTING UI
+#
+
+
+def select_parent_children_buttons(self, context):
+ layout = self.layout
+ layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
+ layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
+
+
+def attr_nodes_menu_func(self, context):
+ col = self.layout.column(align=True)
+ col.menu("NODE_MT_nw_node_vertex_color_menu")
+ col.separator()
+
+
+def multipleimages_menu_func(self, context):
+ col = self.layout.column(align=True)
+ col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
+ col.operator(NWAddSequence.bl_idname, text="Image Sequence")
+ col.separator()
+
+
+def bgreset_menu_func(self, context):
+ self.layout.operator(NWResetBG.bl_idname)
+
+
+#
+# REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
+#
+
+addon_keymaps = []
+# kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
+# props entry: (property name, property value)
+kmi_defs = (
+ # MERGE NODES
+ # NWMergeNodes with Ctrl (AUTO).
+ (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
+ (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
+ (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
+ (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
+ (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
+ (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
+ (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
+ (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
+ (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
+ (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
+ (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
+ (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
+ (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
+ (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
+ (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
+ (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
+ (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
+ (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
+ (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
+ (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
+ (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
+ # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
+ (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
+ (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
+ (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
+ (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
+ (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
+ (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
+ (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
+ (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
+ (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
+ (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
+ (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
+ (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
+ (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
+ (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
+ (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
+ (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
+ # NWMergeNodes with Ctrl Shift (MATH)
+ (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
+ (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
+ (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
+ (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
+ (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
+ (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
+ (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
+ (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
+ (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
+ (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
+ (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
+ (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
+ (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
+ (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
+ (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
+ (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
+ (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
+ (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
+ # BATCH CHANGE NODES
+ # NWBatchChangeNodes with Alt
+ (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
+ (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
+ (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
+ (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
+ (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
+ (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
+ (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
+ (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
+ (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
+ (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
+ (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
+ (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
+ (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
+ (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
+ (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
+ (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
+ (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
+ (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
+ (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
+ (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
+ (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
+ (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
+ (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
+ (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
+ (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
+ (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
+ (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
+ (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
+ # LINK ACTIVE TO SELECTED
+ # Don't use names, don't replace links (K)
+ (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
+ (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
+ # Don't use names, replace links (Shift K)
+ (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
+ (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
+ # Use node name, don't replace links (')
+ (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
+ (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
+ # Use node name, replace links (Shift ')
+ (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
+ (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
+ # Don't use names, don't replace links (;)
+ (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
+ (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
+ # Don't use names, replace links (')
+ (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
+ (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
+ # CHANGE MIX FACTOR
+ (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
+ (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
+ (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
+ (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
+ (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
+ (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
+ (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
+ (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
+ (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
+ (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
+ # CLEAR LABEL (Alt L)
+ (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
+ # MODIFY LABEL (Alt Shift L)
+ (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
+ # Copy Label from active to selected
+ (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
+ # DETACH OUTPUTS (Alt Shift D)
+ (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
+ # LINK TO OUTPUT NODE (O)
+ (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
+ # SELECT PARENT/CHILDREN
+ # Select Children
+ (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
+ # Select Parent
+ (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
+ # Add Texture Setup
+ (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
+ # Reset backdrop
+ (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
+ # Delete unused
+ (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
+ # Frame Seleted
+ (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
+ # Swap Outputs
+ (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
+ # Emission Viewer
+ (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
+ # Reload Images
+ (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
+ # Lazy Mix
+ (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, None, "Lazy Mix"),
+ # Lazy Connect
+ (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, False, False, None, "Lazy Connect"),
+ # Lazy Connect with Menu
+ (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
+ # Viewer Tile Center
+ (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
+ # MENUS
+ ('wm.call_menu', 'SPACE', 'PRESS', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"),
+ ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
+ ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
+ ('wm.call_menu', 'EQUAL', 'PRESS', False, True, False, (('name', NWNodeAlignMenu.bl_idname),), "Node alignment menu"),
+ ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
+ ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
+ ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
+)
+
+
+def register():
+ # props
+ bpy.types.Scene.NWBusyDrawing = StringProperty(
+ name="Busy Drawing!",
+ default="",
+ description="An internal property used to store only the first mouse position")
+ bpy.types.Scene.NWLazySource = StringProperty(
+ name="Lazy Source!",
+ default="x",
+ description="An internal property used to store the first node in a Lazy Connect operation")
+ bpy.types.Scene.NWLazyTarget = StringProperty(
+ name="Lazy Target!",
+ default="x",
+ description="An internal property used to store the last node in a Lazy Connect operation")
+ bpy.types.Scene.NWSourceSocket = IntProperty(
+ name="Source Socket!",
+ default=0,
+ description="An internal property used to store the source socket in a Lazy Connect operation")
+
+ bpy.utils.register_module(__name__)
+
+ # keymaps
+ addon_keymaps.clear()
+ kc = bpy.context.window_manager.keyconfigs.addon
+ if kc:
+ km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
+ for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
+ kmi = km.keymap_items.new(identifier, key, action, 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)
+ bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
+ bpy.types.NODE_PT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
+ bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
+ bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
+ bpy.types.NODE_PT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
+ bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
+ bpy.types.NODE_PT_category_CMP_INPUT.prepend(multipleimages_menu_func)
+
+
+def unregister():
+ # props
+ del bpy.types.Scene.NWBusyDrawing
+ del bpy.types.Scene.NWLazySource
+ del bpy.types.Scene.NWLazyTarget
+ del bpy.types.Scene.NWSourceSocket
+
+ bpy.utils.unregister_module(__name__)
+
+ # keymaps
+ for km, kmi in addon_keymaps:
+ km.keymap_items.remove(kmi)
+ addon_keymaps.clear()
+
+ # menuitems
+ bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
+ bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
+ bpy.types.NODE_PT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
+ bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
+ bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
+ bpy.types.NODE_PT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
+ bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
+ bpy.types.NODE_PT_category_CMP_INPUT.remove(multipleimages_menu_func)
+
+if __name__ == "__main__":
+ register()