Merge branch 'master' into blender2.8
[blender-addons.git] / node_wrangler.py
blob6c133ccfd80804ba21e7d2343161c32652802ffd
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Node Wrangler",
21 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
22 "version": (3, 35),
23 "blender": (2, 80, 0),
24 "location": "Node Editor Toolbar or Ctrl-Space",
25 "description": "Various tools to enhance and speed up node-based workflow",
26 "warning": "",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Nodes/Nodes_Efficiency_Tools",
29 "category": "Node",
32 import bpy, blf, bgl
33 from bpy.types import Operator, Panel, Menu
34 from bpy.props import (
35 FloatProperty,
36 EnumProperty,
37 BoolProperty,
38 IntProperty,
39 StringProperty,
40 FloatVectorProperty,
41 CollectionProperty,
43 from bpy_extras.io_utils import ImportHelper, ExportHelper
44 from mathutils import Vector
45 from math import cos, sin, pi, hypot
46 from os import path
47 from glob import glob
48 from copy import copy
49 from itertools import chain
50 import re
51 from collections import namedtuple
53 #################
54 # rl_outputs:
55 # list of outputs of Input Render Layer
56 # with attributes determinig if pass is used,
57 # and MultiLayer EXR outputs names and corresponding render engines
59 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_internal, in_cycles)
60 RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_internal', 'in_cycles'])
61 rl_outputs = (
62 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
63 RL_entry('use_pass_color', 'Color', 'Color', True, False),
64 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
65 RL_entry('use_pass_diffuse', 'Diffuse', 'Diffuse', True, False),
66 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
67 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
68 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
69 RL_entry('use_pass_emit', 'Emit', 'Emit', True, False),
70 RL_entry('use_pass_environment', 'Environment', 'Env', True, False),
71 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
72 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
73 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
74 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', True, False),
75 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', True, True),
76 RL_entry('use_pass_mist', 'Mist', 'Mist', True, False),
77 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
78 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', True, True),
79 RL_entry('use_pass_reflection', 'Reflect', 'Reflect', True, False),
80 RL_entry('use_pass_refraction', 'Refract', 'Refract', True, False),
81 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', True, True),
82 RL_entry('use_pass_specular', 'Specular', 'Spec', True, False),
83 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', False, True),
84 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', False, True),
85 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
86 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
87 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
88 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
89 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
90 RL_entry('use_pass_vector', 'Speed', 'Vector', True, True),
91 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
94 # shader nodes
95 # (rna_type.identifier, type, rna_type.name)
96 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
97 shaders_input_nodes_props = (
98 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
99 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
100 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
101 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
102 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
103 ('ShaderNodeRGB', 'RGB', 'RGB'),
104 ('ShaderNodeValue', 'VALUE', 'Value'),
105 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
106 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
107 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
108 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
109 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
110 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
111 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
112 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
114 # (rna_type.identifier, type, rna_type.name)
115 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
116 shaders_output_nodes_props = (
117 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
118 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
119 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
121 # (rna_type.identifier, type, rna_type.name)
122 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
123 shaders_shader_nodes_props = (
124 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
125 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
126 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
127 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
128 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
129 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
130 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
131 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
132 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
133 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
134 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
135 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
136 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
137 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
138 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
139 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
140 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
141 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
142 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
143 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
145 # (rna_type.identifier, type, rna_type.name)
146 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
147 shaders_texture_nodes_props = (
148 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
149 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
150 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
151 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
152 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
153 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
154 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
155 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
156 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
157 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
158 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
159 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
161 # (rna_type.identifier, type, rna_type.name)
162 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
163 shaders_color_nodes_props = (
164 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
165 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
166 ('ShaderNodeInvert', 'INVERT', 'Invert'),
167 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
168 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
169 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
170 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
172 # (rna_type.identifier, type, rna_type.name)
173 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
174 shaders_vector_nodes_props = (
175 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
176 ('ShaderNodeBump', 'BUMP', 'Bump'),
177 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
178 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
179 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
180 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
182 # (rna_type.identifier, type, rna_type.name)
183 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
184 shaders_converter_nodes_props = (
185 ('ShaderNodeMath', 'MATH', 'Math'),
186 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
187 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
188 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
189 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
190 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
191 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
192 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
193 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
194 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
195 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
196 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
198 # (rna_type.identifier, type, rna_type.name)
199 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
200 shaders_layout_nodes_props = (
201 ('NodeFrame', 'FRAME', 'Frame'),
202 ('NodeReroute', 'REROUTE', 'Reroute'),
205 # compositing nodes
206 # (rna_type.identifier, type, rna_type.name)
207 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
208 compo_input_nodes_props = (
209 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
210 ('CompositorNodeImage', 'IMAGE', 'Image'),
211 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
212 ('CompositorNodeMask', 'MASK', 'Mask'),
213 ('CompositorNodeRGB', 'RGB', 'RGB'),
214 ('CompositorNodeValue', 'VALUE', 'Value'),
215 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
216 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
217 ('CompositorNodeTime', 'TIME', 'Time'),
218 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
220 # (rna_type.identifier, type, rna_type.name)
221 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
222 compo_output_nodes_props = (
223 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
224 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
225 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
226 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
227 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
229 # (rna_type.identifier, type, rna_type.name)
230 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
231 compo_color_nodes_props = (
232 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
233 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
234 ('CompositorNodeInvert', 'INVERT', 'Invert'),
235 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
236 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
237 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
238 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
239 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
240 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
241 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
242 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
243 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
245 # (rna_type.identifier, type, rna_type.name)
246 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
247 compo_converter_nodes_props = (
248 ('CompositorNodeMath', 'MATH', 'Math'),
249 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
250 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
251 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
252 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
253 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
254 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
255 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
256 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
257 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
258 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
259 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
260 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
261 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
263 # (rna_type.identifier, type, rna_type.name)
264 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
265 compo_filter_nodes_props = (
266 ('CompositorNodeBlur', 'BLUR', 'Blur'),
267 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
268 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
269 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
270 ('CompositorNodeFilter', 'FILTER', 'Filter'),
271 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
272 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
273 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
274 ('CompositorNodeGlare', 'GLARE', 'Glare'),
275 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
276 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
277 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
278 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
280 # (rna_type.identifier, type, rna_type.name)
281 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
282 compo_vector_nodes_props = (
283 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
284 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
285 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
286 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
287 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
289 # (rna_type.identifier, type, rna_type.name)
290 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
291 compo_matte_nodes_props = (
292 ('CompositorNodeKeying', 'KEYING', 'Keying'),
293 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
294 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
295 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
296 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
297 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
298 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
299 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
300 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
301 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
302 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
303 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
305 # (rna_type.identifier, type, rna_type.name)
306 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
307 compo_distort_nodes_props = (
308 ('CompositorNodeScale', 'SCALE', 'Scale'),
309 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
310 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
311 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
312 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
313 ('CompositorNodeFlip', 'FLIP', 'Flip'),
314 ('CompositorNodeCrop', 'CROP', 'Crop'),
315 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
316 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
317 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
318 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
319 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
320 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
322 # (rna_type.identifier, type, rna_type.name)
323 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
324 compo_layout_nodes_props = (
325 ('NodeFrame', 'FRAME', 'Frame'),
326 ('NodeReroute', 'REROUTE', 'Reroute'),
327 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
329 # Blender Render material nodes
330 # (rna_type.identifier, type, rna_type.name)
331 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
332 blender_mat_input_nodes_props = (
333 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
334 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
335 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
336 ('ShaderNodeValue', 'VALUE', 'Value'),
337 ('ShaderNodeRGB', 'RGB', 'RGB'),
338 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
339 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
340 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
343 # (rna_type.identifier, type, rna_type.name)
344 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
345 blender_mat_output_nodes_props = (
346 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
349 # (rna_type.identifier, type, rna_type.name)
350 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
351 blender_mat_color_nodes_props = (
352 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
353 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
354 ('ShaderNodeInvert', 'INVERT', 'Invert'),
355 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
358 # (rna_type.identifier, type, rna_type.name)
359 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
360 blender_mat_vector_nodes_props = (
361 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
362 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
363 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
366 # (rna_type.identifier, type, rna_type.name)
367 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
368 blender_mat_converter_nodes_props = (
369 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
370 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
371 ('ShaderNodeMath', 'MATH', 'Math'),
372 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
373 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
374 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
375 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
376 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
377 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
380 # (rna_type.identifier, type, rna_type.name)
381 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
382 blender_mat_layout_nodes_props = (
383 ('NodeReroute', 'REROUTE', 'Reroute'),
386 # Texture Nodes
387 # (rna_type.identifier, type, rna_type.name)
388 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
389 texture_input_nodes_props = (
390 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
391 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
392 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
393 ('TextureNodeImage', 'IMAGE', 'Image'),
396 # (rna_type.identifier, type, rna_type.name)
397 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
398 texture_output_nodes_props = (
399 ('TextureNodeOutput', 'OUTPUT', 'Output'),
400 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
403 # (rna_type.identifier, type, rna_type.name)
404 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
405 texture_color_nodes_props = (
406 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
407 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
408 ('TextureNodeInvert', 'INVERT', 'Invert'),
409 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
410 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
411 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
414 # (rna_type.identifier, type, rna_type.name)
415 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
416 texture_pattern_nodes_props = (
417 ('TextureNodeChecker', 'CHECKER', 'Checker'),
418 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
421 # (rna_type.identifier, type, rna_type.name)
422 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
423 texture_textures_nodes_props = (
424 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
425 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
426 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
427 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
428 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
429 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
430 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
431 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
432 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
433 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
436 # (rna_type.identifier, type, rna_type.name)
437 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
438 texture_converter_nodes_props = (
439 ('TextureNodeMath', 'MATH', 'Math'),
440 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
441 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
442 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
443 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
446 # (rna_type.identifier, type, rna_type.name)
447 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
448 texture_distort_nodes_props = (
449 ('TextureNodeScale', 'SCALE', 'Scale'),
450 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
451 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
452 ('TextureNodeAt', 'AT', 'At'),
455 # (rna_type.identifier, type, rna_type.name)
456 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
457 texture_layout_nodes_props = (
458 ('NodeReroute', 'REROUTE', 'Reroute'),
461 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
462 # used list, not tuple for easy merging with other lists.
463 blend_types = [
464 ('MIX', 'Mix', 'Mix Mode'),
465 ('ADD', 'Add', 'Add Mode'),
466 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
467 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
468 ('SCREEN', 'Screen', 'Screen Mode'),
469 ('DIVIDE', 'Divide', 'Divide Mode'),
470 ('DIFFERENCE', 'Difference', 'Difference Mode'),
471 ('DARKEN', 'Darken', 'Darken Mode'),
472 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
473 ('OVERLAY', 'Overlay', 'Overlay Mode'),
474 ('DODGE', 'Dodge', 'Dodge Mode'),
475 ('BURN', 'Burn', 'Burn Mode'),
476 ('HUE', 'Hue', 'Hue Mode'),
477 ('SATURATION', 'Saturation', 'Saturation Mode'),
478 ('VALUE', 'Value', 'Value Mode'),
479 ('COLOR', 'Color', 'Color Mode'),
480 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
481 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
484 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
485 # used list, not tuple for easy merging with other lists.
486 operations = [
487 ('ADD', 'Add', 'Add Mode'),
488 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
489 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
490 ('DIVIDE', 'Divide', 'Divide Mode'),
491 ('SINE', 'Sine', 'Sine Mode'),
492 ('COSINE', 'Cosine', 'Cosine Mode'),
493 ('TANGENT', 'Tangent', 'Tangent Mode'),
494 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
495 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
496 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
497 ('POWER', 'Power', 'Power Mode'),
498 ('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
499 ('MINIMUM', 'Minimum', 'Minimum Mode'),
500 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
501 ('ROUND', 'Round', 'Round Mode'),
502 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
503 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
504 ('MODULO', 'Modulo', 'Modulo Mode'),
505 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
508 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
509 # used list, not tuple for easy merging with other lists.
510 navs = [
511 ('CURRENT', 'Current', 'Leave at current state'),
512 ('NEXT', 'Next', 'Next blend type/operation'),
513 ('PREV', 'Prev', 'Previous blend type/operation'),
516 draw_color_sets = {
517 "red_white": (
518 (1.0, 1.0, 1.0, 0.7),
519 (1.0, 0.0, 0.0, 0.7),
520 (0.8, 0.2, 0.2, 1.0)
522 "green": (
523 (0.0, 0.0, 0.0, 1.0),
524 (0.38, 0.77, 0.38, 1.0),
525 (0.38, 0.77, 0.38, 1.0)
527 "yellow": (
528 (0.0, 0.0, 0.0, 1.0),
529 (0.77, 0.77, 0.16, 1.0),
530 (0.77, 0.77, 0.16, 1.0)
532 "purple": (
533 (0.0, 0.0, 0.0, 1.0),
534 (0.38, 0.38, 0.77, 1.0),
535 (0.38, 0.38, 0.77, 1.0)
537 "grey": (
538 (0.0, 0.0, 0.0, 1.0),
539 (0.63, 0.63, 0.63, 1.0),
540 (0.63, 0.63, 0.63, 1.0)
542 "black": (
543 (1.0, 1.0, 1.0, 0.7),
544 (0.0, 0.0, 0.0, 0.7),
545 (0.2, 0.2, 0.2, 1.0)
550 def is_cycles_or_eevee(context):
551 return context.scene.render.engine in {'CYCLES', 'BLENDER_EEVEE'}
554 def nice_hotkey_name(punc):
555 # convert the ugly string name into the actual character
556 pairs = (
557 ('LEFTMOUSE', "LMB"),
558 ('MIDDLEMOUSE', "MMB"),
559 ('RIGHTMOUSE', "RMB"),
560 ('SELECTMOUSE', "Select"),
561 ('WHEELUPMOUSE', "Wheel Up"),
562 ('WHEELDOWNMOUSE', "Wheel Down"),
563 ('WHEELINMOUSE', "Wheel In"),
564 ('WHEELOUTMOUSE', "Wheel Out"),
565 ('ZERO', "0"),
566 ('ONE', "1"),
567 ('TWO', "2"),
568 ('THREE', "3"),
569 ('FOUR', "4"),
570 ('FIVE', "5"),
571 ('SIX', "6"),
572 ('SEVEN', "7"),
573 ('EIGHT', "8"),
574 ('NINE', "9"),
575 ('OSKEY', "Super"),
576 ('RET', "Enter"),
577 ('LINE_FEED', "Enter"),
578 ('SEMI_COLON', ";"),
579 ('PERIOD', "."),
580 ('COMMA', ","),
581 ('QUOTE', '"'),
582 ('MINUS', "-"),
583 ('SLASH', "/"),
584 ('BACK_SLASH', "\\"),
585 ('EQUAL', "="),
586 ('NUMPAD_1', "Numpad 1"),
587 ('NUMPAD_2', "Numpad 2"),
588 ('NUMPAD_3', "Numpad 3"),
589 ('NUMPAD_4', "Numpad 4"),
590 ('NUMPAD_5', "Numpad 5"),
591 ('NUMPAD_6', "Numpad 6"),
592 ('NUMPAD_7', "Numpad 7"),
593 ('NUMPAD_8', "Numpad 8"),
594 ('NUMPAD_9', "Numpad 9"),
595 ('NUMPAD_0', "Numpad 0"),
596 ('NUMPAD_PERIOD', "Numpad ."),
597 ('NUMPAD_SLASH', "Numpad /"),
598 ('NUMPAD_ASTERIX', "Numpad *"),
599 ('NUMPAD_MINUS', "Numpad -"),
600 ('NUMPAD_ENTER', "Numpad Enter"),
601 ('NUMPAD_PLUS', "Numpad +"),
603 nice_punc = False
604 for (ugly, nice) in pairs:
605 if punc == ugly:
606 nice_punc = nice
607 break
608 if not nice_punc:
609 nice_punc = punc.replace("_", " ").title()
610 return nice_punc
613 def force_update(context):
614 context.space_data.node_tree.update_tag()
617 def dpifac():
618 prefs = bpy.context.user_preferences.system
619 return prefs.dpi * prefs.pixel_size / 72
622 def node_mid_pt(node, axis):
623 if axis == 'x':
624 d = node.location.x + (node.dimensions.x / 2)
625 elif axis == 'y':
626 d = node.location.y - (node.dimensions.y / 2)
627 else:
628 d = 0
629 return d
632 def autolink(node1, node2, links):
633 link_made = False
635 for outp in node1.outputs:
636 for inp in node2.inputs:
637 if not inp.is_linked and inp.name == outp.name:
638 link_made = True
639 links.new(outp, inp)
640 return True
642 for outp in node1.outputs:
643 for inp in node2.inputs:
644 if not inp.is_linked and inp.type == outp.type:
645 link_made = True
646 links.new(outp, inp)
647 return True
649 # force some connection even if the type doesn't match
650 for outp in node1.outputs:
651 for inp in node2.inputs:
652 if not inp.is_linked:
653 link_made = True
654 links.new(outp, inp)
655 return True
657 # even if no sockets are open, force one of matching type
658 for outp in node1.outputs:
659 for inp in node2.inputs:
660 if inp.type == outp.type:
661 link_made = True
662 links.new(outp, inp)
663 return True
665 # do something!
666 for outp in node1.outputs:
667 for inp in node2.inputs:
668 link_made = True
669 links.new(outp, inp)
670 return True
672 print("Could not make a link from " + node1.name + " to " + node2.name)
673 return link_made
676 def node_at_pos(nodes, context, event):
677 nodes_near_mouse = []
678 nodes_under_mouse = []
679 target_node = None
681 store_mouse_cursor(context, event)
682 x, y = context.space_data.cursor_location
683 x = x
684 y = y
686 # Make a list of each corner (and middle of border) for each node.
687 # Will be sorted to find nearest point and thus nearest node
688 node_points_with_dist = []
689 for node in nodes:
690 skipnode = False
691 if node.type != 'FRAME': # no point trying to link to a frame node
692 locx = node.location.x
693 locy = node.location.y
694 dimx = node.dimensions.x/dpifac()
695 dimy = node.dimensions.y/dpifac()
696 if node.parent:
697 locx += node.parent.location.x
698 locy += node.parent.location.y
699 if node.parent.parent:
700 locx += node.parent.parent.location.x
701 locy += node.parent.parent.location.y
702 if node.parent.parent.parent:
703 locx += node.parent.parent.parent.location.x
704 locy += node.parent.parent.parent.location.y
705 if node.parent.parent.parent.parent:
706 # Support three levels or parenting
707 # There's got to be a better way to do this...
708 skipnode = True
709 if not skipnode:
710 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
711 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
712 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
713 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
715 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
716 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
717 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
718 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
720 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
722 for node in nodes:
723 if node.type != 'FRAME' and skipnode == False:
724 locx = node.location.x
725 locy = node.location.y
726 dimx = node.dimensions.x/dpifac()
727 dimy = node.dimensions.y/dpifac()
728 if node.parent:
729 locx += node.parent.location.x
730 locy += node.parent.location.y
731 if (locx <= x <= locx + dimx) and \
732 (locy - dimy <= y <= locy):
733 nodes_under_mouse.append(node)
735 if len(nodes_under_mouse) == 1:
736 if nodes_under_mouse[0] != nearest_node:
737 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
738 else:
739 target_node = nearest_node # else use the nearest node
740 else:
741 target_node = nearest_node
742 return target_node
745 def store_mouse_cursor(context, event):
746 space = context.space_data
747 v2d = context.region.view2d
748 tree = space.edit_tree
750 # convert mouse position to the View2D for later node placement
751 if context.region.type == 'WINDOW':
752 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
753 else:
754 space.cursor_location = tree.view_center
757 def draw_line(x1, y1, x2, y2, size, colour=[1.0, 1.0, 1.0, 0.7]):
758 shademodel_state = bgl.Buffer(bgl.GL_INT, 1)
759 bgl.glGetIntegerv(bgl.GL_SHADE_MODEL, shademodel_state)
761 bgl.glEnable(bgl.GL_BLEND)
762 bgl.glLineWidth(size * dpifac())
763 bgl.glShadeModel(bgl.GL_SMOOTH)
764 bgl.glEnable(bgl.GL_LINE_SMOOTH)
766 bgl.glBegin(bgl.GL_LINE_STRIP)
767 try:
768 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)
769 bgl.glVertex2f(x1, y1)
770 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
771 bgl.glVertex2f(x2, y2)
772 except:
773 pass
774 bgl.glEnd()
776 bgl.glShadeModel(shademodel_state[0])
777 bgl.glDisable(bgl.GL_LINE_SMOOTH)
780 def draw_circle(mx, my, radius, colour=[1.0, 1.0, 1.0, 0.7]):
781 bgl.glEnable(bgl.GL_LINE_SMOOTH)
782 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
783 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
784 radius = radius * dpifac()
785 sides = 12
786 for i in range(sides + 1):
787 cosine = radius * cos(i * 2 * pi / sides) + mx
788 sine = radius * sin(i * 2 * pi / sides) + my
789 bgl.glVertex2f(cosine, sine)
790 bgl.glEnd()
791 bgl.glDisable(bgl.GL_LINE_SMOOTH)
794 def draw_rounded_node_border(node, radius=8, colour=[1.0, 1.0, 1.0, 0.7]):
795 bgl.glEnable(bgl.GL_BLEND)
796 bgl.glEnable(bgl.GL_LINE_SMOOTH)
798 area_width = bpy.context.area.width - (16*dpifac()) - 1
799 bottom_bar = (16*dpifac()) + 1
800 sides = 16
801 radius = radius*dpifac()
802 bgl.glColor4f(colour[0], colour[1], colour[2], colour[3])
804 nlocx = (node.location.x+1)*dpifac()
805 nlocy = (node.location.y+1)*dpifac()
806 ndimx = node.dimensions.x
807 ndimy = node.dimensions.y
808 # This is a stupid way to do this... TODO use while loop
809 if node.parent:
810 nlocx += node.parent.location.x
811 nlocy += node.parent.location.y
812 if node.parent.parent:
813 nlocx += node.parent.parent.location.x
814 nlocy += node.parent.parent.location.y
815 if node.parent.parent.parent:
816 nlocx += node.parent.parent.parent.location.x
817 nlocy += node.parent.parent.parent.location.y
819 if node.hide:
820 nlocx += -1
821 nlocy += 5
822 if node.type == 'REROUTE':
823 #nlocx += 1
824 nlocy -= 1
825 ndimx = 0
826 ndimy = 0
827 radius += 6
829 # Top left corner
830 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
831 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
832 bgl.glVertex2f(mx,my)
833 for i in range(sides+1):
834 if (4<=i<=8):
835 if my > bottom_bar and mx < area_width:
836 cosine = radius * cos(i * 2 * pi / sides) + mx
837 sine = radius * sin(i * 2 * pi / sides) + my
838 bgl.glVertex2f(cosine, sine)
839 bgl.glEnd()
841 # Top right corner
842 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
843 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
844 bgl.glVertex2f(mx,my)
845 for i in range(sides+1):
846 if (0<=i<=4):
847 if my > bottom_bar and mx < area_width:
848 cosine = radius * cos(i * 2 * pi / sides) + mx
849 sine = radius * sin(i * 2 * pi / sides) + my
850 bgl.glVertex2f(cosine, sine)
851 bgl.glEnd()
853 # Bottom left corner
854 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
855 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
856 bgl.glVertex2f(mx,my)
857 for i in range(sides+1):
858 if (8<=i<=12):
859 if my > bottom_bar and mx < area_width:
860 cosine = radius * cos(i * 2 * pi / sides) + mx
861 sine = radius * sin(i * 2 * pi / sides) + my
862 bgl.glVertex2f(cosine, sine)
863 bgl.glEnd()
865 # Bottom right corner
866 bgl.glBegin(bgl.GL_TRIANGLE_FAN)
867 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
868 bgl.glVertex2f(mx,my)
869 for i in range(sides+1):
870 if (12<=i<=16):
871 if my > bottom_bar and mx < area_width:
872 cosine = radius * cos(i * 2 * pi / sides) + mx
873 sine = radius * sin(i * 2 * pi / sides) + my
874 bgl.glVertex2f(cosine, sine)
875 bgl.glEnd()
878 # Left edge
879 bgl.glBegin(bgl.GL_QUADS)
880 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
881 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
882 m1y = max(m1y, bottom_bar)
883 m2y = max(m2y, bottom_bar)
884 if m1x < area_width and m2x < area_width:
885 bgl.glVertex2f(m2x-radius,m2y) # draw order is important, start with bottom left and go anti-clockwise
886 bgl.glVertex2f(m2x,m2y)
887 bgl.glVertex2f(m1x,m1y)
888 bgl.glVertex2f(m1x-radius,m1y)
889 bgl.glEnd()
891 # Top edge
892 bgl.glBegin(bgl.GL_QUADS)
893 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
894 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
895 m1x = min(m1x, area_width)
896 m2x = min(m2x, area_width)
897 if m1y > bottom_bar and m2y > bottom_bar:
898 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
899 bgl.glVertex2f(m2x,m2y)
900 bgl.glVertex2f(m2x,m1y+radius)
901 bgl.glVertex2f(m1x,m1y+radius)
902 bgl.glEnd()
904 # Right edge
905 bgl.glBegin(bgl.GL_QUADS)
906 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
907 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
908 m1y = max(m1y, bottom_bar)
909 m2y = max(m2y, bottom_bar)
910 if m1x < area_width and m2x < area_width:
911 bgl.glVertex2f(m2x,m2y) # draw order is important, start with bottom left and go anti-clockwise
912 bgl.glVertex2f(m2x+radius,m2y)
913 bgl.glVertex2f(m1x+radius,m1y)
914 bgl.glVertex2f(m1x,m1y)
915 bgl.glEnd()
917 # Bottom edge
918 bgl.glBegin(bgl.GL_QUADS)
919 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
920 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
921 m1x = min(m1x, area_width)
922 m2x = min(m2x, area_width)
923 if m1y > bottom_bar and m2y > bottom_bar:
924 bgl.glVertex2f(m1x,m2y) # draw order is important, start with bottom left and go anti-clockwise
925 bgl.glVertex2f(m2x,m2y)
926 bgl.glVertex2f(m2x,m1y-radius)
927 bgl.glVertex2f(m1x,m1y-radius)
928 bgl.glEnd()
931 # Restore defaults
932 bgl.glDisable(bgl.GL_BLEND)
933 bgl.glDisable(bgl.GL_LINE_SMOOTH)
936 def draw_callback_nodeoutline(self, context, mode):
937 if self.mouse_path:
938 nodes, links = get_nodes_links(context)
939 bgl.glEnable(bgl.GL_LINE_SMOOTH)
941 if mode == "LINK":
942 col_outer = [1.0, 0.2, 0.2, 0.4]
943 col_inner = [0.0, 0.0, 0.0, 0.5]
944 col_circle_inner = [0.3, 0.05, 0.05, 1.0]
945 elif mode == "LINKMENU":
946 col_outer = [0.4, 0.6, 1.0, 0.4]
947 col_inner = [0.0, 0.0, 0.0, 0.5]
948 col_circle_inner = [0.08, 0.15, .3, 1.0]
949 elif mode == "MIX":
950 col_outer = [0.2, 1.0, 0.2, 0.4]
951 col_inner = [0.0, 0.0, 0.0, 0.5]
952 col_circle_inner = [0.05, 0.3, 0.05, 1.0]
954 m1x = self.mouse_path[0][0]
955 m1y = self.mouse_path[0][1]
956 m2x = self.mouse_path[-1][0]
957 m2y = self.mouse_path[-1][1]
959 n1 = nodes[context.scene.NWLazySource]
960 n2 = nodes[context.scene.NWLazyTarget]
962 if n1 == n2:
963 col_outer = [0.4, 0.4, 0.4, 0.4]
964 col_inner = [0.0, 0.0, 0.0, 0.5]
965 col_circle_inner = [0.2, 0.2, 0.2, 1.0]
967 draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
968 draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
969 draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
970 draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
972 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
973 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
975 # circle outline
976 draw_circle(m1x, m1y, 7, col_outer)
977 draw_circle(m2x, m2y, 7, col_outer)
979 # circle inner
980 draw_circle(m1x, m1y, 5, col_circle_inner)
981 draw_circle(m2x, m2y, 5, col_circle_inner)
983 # restore opengl defaults
984 bgl.glLineWidth(1)
985 bgl.glDisable(bgl.GL_BLEND)
986 bgl.glColor4f(0.0, 0.0, 0.0, 1.0)
988 bgl.glDisable(bgl.GL_LINE_SMOOTH)
991 def get_nodes_links(context):
992 tree = context.space_data.node_tree
994 # Get nodes from currently edited tree.
995 # If user is editing a group, space_data.node_tree is still the base level (outside group).
996 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
997 # the same as context.active_node, the user is in a group.
998 # Check recursively until we find the real active node_tree:
999 if tree.nodes.active:
1000 while tree.nodes.active != context.active_node:
1001 tree = tree.nodes.active.node_tree
1003 return tree.nodes, tree.links
1005 # Principled prefs
1006 class NWPrincipledPreferences(bpy.types.PropertyGroup):
1007 base_color: StringProperty(
1008 name='Base Color',
1009 default='diffuse diff albedo base col color',
1010 description='Naming Components for Base Color maps')
1011 sss_color: StringProperty(
1012 name='Subsurface Color',
1013 default='sss subsurface',
1014 description='Naming Components for Subsurface Color maps')
1015 metallic: StringProperty(
1016 name='Metallic',
1017 default='metallic metalness metal mtl',
1018 description='Naming Components for metallness maps')
1019 specular: StringProperty(
1020 name='Specular',
1021 default='specularity specular spec spc',
1022 description='Naming Components for Specular maps')
1023 normal: StringProperty(
1024 name='Normal',
1025 default='normal nor nrm nrml norm',
1026 description='Naming Components for Normal maps')
1027 bump: StringProperty(
1028 name='Bump',
1029 default='bump bmp',
1030 description='Naming Components for bump maps')
1031 rough: StringProperty(
1032 name='Roughness',
1033 default='roughness rough rgh',
1034 description='Naming Components for roughness maps')
1035 gloss: StringProperty(
1036 name='Gloss',
1037 default='gloss glossy glossyness',
1038 description='Naming Components for glossy maps')
1039 displacement: StringProperty(
1040 name='Displacement',
1041 default='displacement displace disp dsp height heightmap',
1042 description='Naming Components for displacement maps')
1044 # Addon prefs
1045 class NWNodeWrangler(bpy.types.AddonPreferences):
1046 bl_idname = __name__
1048 merge_hide: EnumProperty(
1049 name="Hide Mix nodes",
1050 items=(
1051 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1052 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1053 ("NEVER", "Never", "Never collapse the new merge nodes")
1055 default='NON_SHADER',
1056 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded")
1057 merge_position: EnumProperty(
1058 name="Mix Node Position",
1059 items=(
1060 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1061 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1063 default='CENTER',
1064 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes")
1066 show_hotkey_list: BoolProperty(
1067 name="Show Hotkey List",
1068 default=False,
1069 description="Expand this box into a list of all the hotkeys for functions in this addon"
1071 hotkey_list_filter: StringProperty(
1072 name=" Filter by Name",
1073 default="",
1074 description="Show only hotkeys that have this text in their name"
1076 show_principled_lists: BoolProperty(
1077 name="Show Principled naming tags",
1078 default=False,
1079 description="Expand this box into a list of all naming tags for principled texture setup"
1081 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
1083 def draw(self, context):
1084 layout = self.layout
1085 col = layout.column()
1086 col.prop(self, "merge_position")
1087 col.prop(self, "merge_hide")
1089 box = layout.box()
1090 col = box.column(align=True)
1091 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
1092 if self.show_principled_lists:
1093 tags = self.principled_tags
1095 col.prop(tags, "base_color")
1096 col.prop(tags, "sss_color")
1097 col.prop(tags, "metallic")
1098 col.prop(tags, "specular")
1099 col.prop(tags, "rough")
1100 col.prop(tags, "gloss")
1101 col.prop(tags, "normal")
1102 col.prop(tags, "bump")
1103 col.prop(tags, "displacement")
1105 box = layout.box()
1106 col = box.column(align=True)
1107 hotkey_button_name = "Show Hotkey List"
1108 if self.show_hotkey_list:
1109 hotkey_button_name = "Hide Hotkey List"
1110 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
1111 if self.show_hotkey_list:
1112 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
1113 col.separator()
1114 for hotkey in kmi_defs:
1115 if hotkey[7]:
1116 hotkey_name = hotkey[7]
1118 if self.hotkey_list_filter.lower() in hotkey_name.lower():
1119 row = col.row(align=True)
1120 row.label(hotkey_name)
1121 keystr = nice_hotkey_name(hotkey[1])
1122 if hotkey[4]:
1123 keystr = "Shift " + keystr
1124 if hotkey[5]:
1125 keystr = "Alt " + keystr
1126 if hotkey[3]:
1127 keystr = "Ctrl " + keystr
1128 row.label(keystr)
1132 def nw_check(context):
1133 space = context.space_data
1134 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree"]
1136 valid = False
1137 if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
1138 valid = True
1140 return valid
1142 class NWBase:
1143 @classmethod
1144 def poll(cls, context):
1145 return nw_check(context)
1148 # OPERATORS
1149 class NWLazyMix(Operator, NWBase):
1150 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1151 bl_idname = "node.nw_lazy_mix"
1152 bl_label = "Mix Nodes"
1153 bl_options = {'REGISTER', 'UNDO'}
1155 def modal(self, context, event):
1156 context.area.tag_redraw()
1157 nodes, links = get_nodes_links(context)
1158 cont = True
1160 start_pos = [event.mouse_region_x, event.mouse_region_y]
1162 node1 = None
1163 if not context.scene.NWBusyDrawing:
1164 node1 = node_at_pos(nodes, context, event)
1165 if node1:
1166 context.scene.NWBusyDrawing = node1.name
1167 else:
1168 if context.scene.NWBusyDrawing != 'STOP':
1169 node1 = nodes[context.scene.NWBusyDrawing]
1171 context.scene.NWLazySource = node1.name
1172 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1174 if event.type == 'MOUSEMOVE':
1175 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1177 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1178 end_pos = [event.mouse_region_x, event.mouse_region_y]
1179 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1181 node2 = None
1182 node2 = node_at_pos(nodes, context, event)
1183 if node2:
1184 context.scene.NWBusyDrawing = node2.name
1186 if node1 == node2:
1187 cont = False
1189 if cont:
1190 if node1 and node2:
1191 for node in nodes:
1192 node.select = False
1193 node1.select = True
1194 node2.select = True
1196 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
1198 context.scene.NWBusyDrawing = ""
1199 return {'FINISHED'}
1201 elif event.type == 'ESC':
1202 print('cancelled')
1203 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1204 return {'CANCELLED'}
1206 return {'RUNNING_MODAL'}
1208 def invoke(self, context, event):
1209 if context.area.type == 'NODE_EDITOR':
1210 # the arguments we pass the the callback
1211 args = (self, context, 'MIX')
1212 # Add the region OpenGL drawing callback
1213 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1214 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1216 self.mouse_path = []
1218 context.window_manager.modal_handler_add(self)
1219 return {'RUNNING_MODAL'}
1220 else:
1221 self.report({'WARNING'}, "View3D not found, cannot run operator")
1222 return {'CANCELLED'}
1225 class NWLazyConnect(Operator, NWBase):
1226 """Connect two nodes without clicking a specific socket (automatically determined"""
1227 bl_idname = "node.nw_lazy_connect"
1228 bl_label = "Lazy Connect"
1229 bl_options = {'REGISTER', 'UNDO'}
1230 with_menu: BoolProperty()
1232 def modal(self, context, event):
1233 context.area.tag_redraw()
1234 nodes, links = get_nodes_links(context)
1235 cont = True
1237 start_pos = [event.mouse_region_x, event.mouse_region_y]
1239 node1 = None
1240 if not context.scene.NWBusyDrawing:
1241 node1 = node_at_pos(nodes, context, event)
1242 if node1:
1243 context.scene.NWBusyDrawing = node1.name
1244 else:
1245 if context.scene.NWBusyDrawing != 'STOP':
1246 node1 = nodes[context.scene.NWBusyDrawing]
1248 context.scene.NWLazySource = node1.name
1249 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1251 if event.type == 'MOUSEMOVE':
1252 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1254 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1255 end_pos = [event.mouse_region_x, event.mouse_region_y]
1256 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1258 node2 = None
1259 node2 = node_at_pos(nodes, context, event)
1260 if node2:
1261 context.scene.NWBusyDrawing = node2.name
1263 if node1 == node2:
1264 cont = False
1266 link_success = False
1267 if cont:
1268 if node1 and node2:
1269 original_sel = []
1270 original_unsel = []
1271 for node in nodes:
1272 if node.select == True:
1273 node.select = False
1274 original_sel.append(node)
1275 else:
1276 original_unsel.append(node)
1277 node1.select = True
1278 node2.select = True
1280 #link_success = autolink(node1, node2, links)
1281 if self.with_menu:
1282 if len(node1.outputs) > 1 and node2.inputs:
1283 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1284 elif len(node1.outputs) == 1:
1285 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1286 else:
1287 link_success = autolink(node1, node2, links)
1289 for node in original_sel:
1290 node.select = True
1291 for node in original_unsel:
1292 node.select = False
1294 if link_success:
1295 force_update(context)
1296 context.scene.NWBusyDrawing = ""
1297 return {'FINISHED'}
1299 elif event.type == 'ESC':
1300 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1301 return {'CANCELLED'}
1303 return {'RUNNING_MODAL'}
1305 def invoke(self, context, event):
1306 if context.area.type == 'NODE_EDITOR':
1307 nodes, links = get_nodes_links(context)
1308 node = node_at_pos(nodes, context, event)
1309 if node:
1310 context.scene.NWBusyDrawing = node.name
1312 # the arguments we pass the the callback
1313 mode = "LINK"
1314 if self.with_menu:
1315 mode = "LINKMENU"
1316 args = (self, context, mode)
1317 # Add the region OpenGL drawing callback
1318 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1319 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1321 self.mouse_path = []
1323 context.window_manager.modal_handler_add(self)
1324 return {'RUNNING_MODAL'}
1325 else:
1326 self.report({'WARNING'}, "View3D not found, cannot run operator")
1327 return {'CANCELLED'}
1330 class NWDeleteUnused(Operator, NWBase):
1331 """Delete all nodes whose output is not used"""
1332 bl_idname = 'node.nw_del_unused'
1333 bl_label = 'Delete Unused Nodes'
1334 bl_options = {'REGISTER', 'UNDO'}
1336 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1337 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1339 def is_unused_node(self, node):
1340 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1341 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1342 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1343 if node.type in end_types:
1344 return False
1346 for output in node.outputs:
1347 if output.links:
1348 return False
1349 return True
1351 @classmethod
1352 def poll(cls, context):
1353 valid = False
1354 if nw_check(context):
1355 if context.space_data.node_tree.nodes:
1356 valid = True
1357 return valid
1359 def execute(self, context):
1360 nodes, links = get_nodes_links(context)
1362 # Store selection
1363 selection = []
1364 for node in nodes:
1365 if node.select == True:
1366 selection.append(node.name)
1368 for node in nodes:
1369 node.select = False
1371 deleted_nodes = []
1372 temp_deleted_nodes = []
1373 del_unused_iterations = len(nodes)
1374 for it in range(0, del_unused_iterations):
1375 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1376 for node in nodes:
1377 if self.is_unused_node(node):
1378 node.select = True
1379 deleted_nodes.append(node.name)
1380 bpy.ops.node.delete()
1382 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1383 break
1385 if self.delete_frames:
1386 repeat = True
1387 while repeat:
1388 frames_in_use = []
1389 frames = []
1390 repeat = False
1391 for node in nodes:
1392 if node.parent:
1393 frames_in_use.append(node.parent)
1394 for node in nodes:
1395 if node.type == 'FRAME' and node not in frames_in_use:
1396 frames.append(node)
1397 if node.parent:
1398 repeat = True # repeat for nested frames
1399 for node in frames:
1400 if node not in frames_in_use:
1401 node.select = True
1402 deleted_nodes.append(node.name)
1403 bpy.ops.node.delete()
1405 if self.delete_muted:
1406 for node in nodes:
1407 if node.mute:
1408 node.select = True
1409 deleted_nodes.append(node.name)
1410 bpy.ops.node.delete_reconnect()
1412 # get unique list of deleted nodes (iterations would count the same node more than once)
1413 deleted_nodes = list(set(deleted_nodes))
1414 for n in deleted_nodes:
1415 self.report({'INFO'}, "Node " + n + " deleted")
1416 num_deleted = len(deleted_nodes)
1417 n = ' node'
1418 if num_deleted > 1:
1419 n += 's'
1420 if num_deleted:
1421 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1422 else:
1423 self.report({'INFO'}, "Nothing deleted")
1425 # Restore selection
1426 nodes, links = get_nodes_links(context)
1427 for node in nodes:
1428 if node.name in selection:
1429 node.select = True
1430 return {'FINISHED'}
1432 def invoke(self, context, event):
1433 return context.window_manager.invoke_confirm(self, event)
1436 class NWSwapLinks(Operator, NWBase):
1437 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1438 bl_idname = 'node.nw_swap_links'
1439 bl_label = 'Swap Links'
1440 bl_options = {'REGISTER', 'UNDO'}
1442 @classmethod
1443 def poll(cls, context):
1444 valid = False
1445 if nw_check(context):
1446 if context.selected_nodes:
1447 valid = len(context.selected_nodes) <= 2
1448 return valid
1450 def execute(self, context):
1451 nodes, links = get_nodes_links(context)
1452 selected_nodes = context.selected_nodes
1453 n1 = selected_nodes[0]
1455 # Swap outputs
1456 if len(selected_nodes) == 2:
1457 n2 = selected_nodes[1]
1458 if n1.outputs and n2.outputs:
1459 n1_outputs = []
1460 n2_outputs = []
1462 out_index = 0
1463 for output in n1.outputs:
1464 if output.links:
1465 for link in output.links:
1466 n1_outputs.append([out_index, link.to_socket])
1467 links.remove(link)
1468 out_index += 1
1470 out_index = 0
1471 for output in n2.outputs:
1472 if output.links:
1473 for link in output.links:
1474 n2_outputs.append([out_index, link.to_socket])
1475 links.remove(link)
1476 out_index += 1
1478 for connection in n1_outputs:
1479 try:
1480 links.new(n2.outputs[connection[0]], connection[1])
1481 except:
1482 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1483 for connection in n2_outputs:
1484 try:
1485 links.new(n1.outputs[connection[0]], connection[1])
1486 except:
1487 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1488 else:
1489 if n1.outputs or n2.outputs:
1490 self.report({'WARNING'}, "One of the nodes has no outputs!")
1491 else:
1492 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1494 # Swap Inputs
1495 elif len(selected_nodes) == 1:
1496 if n1.inputs:
1497 types = []
1499 for i1 in n1.inputs:
1500 if i1.is_linked:
1501 similar_types = 0
1502 for i2 in n1.inputs:
1503 if i1.type == i2.type and i2.is_linked:
1504 similar_types += 1
1505 types.append ([i1, similar_types, i])
1506 i += 1
1507 types.sort(key=lambda k: k[1], reverse=True)
1509 if types:
1510 t = types[0]
1511 if t[1] == 2:
1512 for i2 in n1.inputs:
1513 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1514 pair = [t[0], i2]
1515 i1f = pair[0].links[0].from_socket
1516 i1t = pair[0].links[0].to_socket
1517 i2f = pair[1].links[0].from_socket
1518 i2t = pair[1].links[0].to_socket
1519 links.new(i1f, i2t)
1520 links.new(i2f, i1t)
1521 if t[1] == 1:
1522 if len(types) == 1:
1523 fs = t[0].links[0].from_socket
1524 i = t[2]
1525 links.remove(t[0].links[0])
1526 if i+1 == len(n1.inputs):
1527 i = -1
1528 i += 1
1529 while n1.inputs[i].is_linked:
1530 i += 1
1531 links.new(fs, n1.inputs[i])
1532 elif len(types) == 2:
1533 i1f = types[0][0].links[0].from_socket
1534 i1t = types[0][0].links[0].to_socket
1535 i2f = types[1][0].links[0].from_socket
1536 i2t = types[1][0].links[0].to_socket
1537 links.new(i1f, i2t)
1538 links.new(i2f, i1t)
1540 else:
1541 self.report({'WARNING'}, "This node has no input connections to swap!")
1542 else:
1543 self.report({'WARNING'}, "This node has no inputs to swap!")
1545 force_update(context)
1546 return {'FINISHED'}
1549 class NWResetBG(Operator, NWBase):
1550 """Reset the zoom and position of the background image"""
1551 bl_idname = 'node.nw_bg_reset'
1552 bl_label = 'Reset Backdrop'
1553 bl_options = {'REGISTER', 'UNDO'}
1555 @classmethod
1556 def poll(cls, context):
1557 valid = False
1558 if nw_check(context):
1559 snode = context.space_data
1560 valid = snode.tree_type == 'CompositorNodeTree'
1561 return valid
1563 def execute(self, context):
1564 context.space_data.backdrop_zoom = 1
1565 context.space_data.backdrop_x = 0
1566 context.space_data.backdrop_y = 0
1567 return {'FINISHED'}
1570 class NWAddAttrNode(Operator, NWBase):
1571 """Add an Attribute node with this name"""
1572 bl_idname = 'node.nw_add_attr_node'
1573 bl_label = 'Add UV map'
1574 attr_name = StringProperty()
1575 bl_options = {'REGISTER', 'UNDO'}
1577 def execute(self, context):
1578 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1579 nodes, links = get_nodes_links(context)
1580 nodes.active.attribute_name = self.attr_name
1581 return {'FINISHED'}
1584 class NWEmissionViewer(Operator, NWBase):
1585 bl_idname = "node.nw_emission_viewer"
1586 bl_label = "Emission Viewer"
1587 bl_description = "Connect active node to Emission Shader for shadeless previews"
1588 bl_options = {'REGISTER', 'UNDO'}
1590 @classmethod
1591 def poll(cls, context):
1592 is_cycles = is_cycles_or_eevee(context)
1593 if nw_check(context):
1594 space = context.space_data
1595 if space.tree_type == 'ShaderNodeTree' and is_cycles:
1596 if context.active_node:
1597 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1598 return True
1599 else:
1600 return True
1601 return False
1603 def invoke(self, context, event):
1604 space = context.space_data
1605 shader_type = space.shader_type
1606 if shader_type == 'OBJECT':
1607 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1608 shader_output_type = "OUTPUT_MATERIAL"
1609 shader_output_ident = "ShaderNodeOutputMaterial"
1610 shader_viewer_ident = "ShaderNodeEmission"
1611 else:
1612 shader_output_type = "OUTPUT_LIGHT"
1613 shader_output_ident = "ShaderNodeOutputLight"
1614 shader_viewer_ident = "ShaderNodeEmission"
1616 elif shader_type == 'WORLD':
1617 shader_output_type = "OUTPUT_WORLD"
1618 shader_output_ident = "ShaderNodeOutputWorld"
1619 shader_viewer_ident = "ShaderNodeBackground"
1620 shader_types = [x[1] for x in shaders_shader_nodes_props]
1621 mlocx = event.mouse_region_x
1622 mlocy = event.mouse_region_y
1623 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1624 if 'FINISHED' in select_node: # only run if mouse click is on a node
1625 nodes, links = get_nodes_links(context)
1626 in_group = context.active_node != space.node_tree.nodes.active
1627 active = nodes.active
1628 output_types = [x[1] for x in shaders_output_nodes_props]
1629 valid = False
1630 if active:
1631 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1632 for out in active.outputs:
1633 if not out.hide:
1634 valid = True
1635 break
1636 if valid:
1637 # get material_output node, store selection, deselect all
1638 materialout = None # placeholder node
1639 selection = []
1640 for node in nodes:
1641 if node.type == shader_output_type:
1642 materialout = node
1643 if node.select:
1644 selection.append(node.name)
1645 node.select = False
1646 if not materialout:
1647 # get right-most location
1648 sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
1649 max_xloc_node = sorted_by_xloc[-1]
1650 if max_xloc_node.name == 'Emission Viewer':
1651 max_xloc_node = sorted_by_xloc[-2]
1653 # get average y location
1654 sum_yloc = 0
1655 for node in nodes:
1656 sum_yloc += node.location.y
1658 new_locx = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1659 new_locy = sum_yloc / len(nodes)
1661 materialout = nodes.new(shader_output_ident)
1662 materialout.location.x = new_locx
1663 materialout.location.y = new_locy
1664 materialout.select = False
1665 # Analyze outputs, add "Emission Viewer" if needed, make links
1666 out_i = None
1667 valid_outputs = []
1668 for i, out in enumerate(active.outputs):
1669 if not out.hide:
1670 valid_outputs.append(i)
1671 if valid_outputs:
1672 out_i = valid_outputs[0] # Start index of node's outputs
1673 for i, valid_i in enumerate(valid_outputs):
1674 for out_link in active.outputs[valid_i].links:
1675 if "Emission Viewer" in out_link.to_node.name or (out_link.to_node == materialout and out_link.to_socket == materialout.inputs[0]):
1676 if i < len(valid_outputs) - 1:
1677 out_i = valid_outputs[i + 1]
1678 else:
1679 out_i = valid_outputs[0]
1680 make_links = [] # store sockets for new links
1681 if active.outputs:
1682 # If output type not 'SHADER' - "Emission Viewer" needed
1683 if active.outputs[out_i].type != 'SHADER':
1684 # get Emission Viewer node
1685 emission_exists = False
1686 emission_placeholder = nodes[0]
1687 for node in nodes:
1688 if "Emission Viewer" in node.name:
1689 emission_exists = True
1690 emission_placeholder = node
1691 if not emission_exists:
1692 emission = nodes.new(shader_viewer_ident)
1693 emission.hide = True
1694 emission.location = [materialout.location.x, (materialout.location.y + 40)]
1695 emission.label = "Viewer"
1696 emission.name = "Emission Viewer"
1697 emission.use_custom_color = True
1698 emission.color = (0.6, 0.5, 0.4)
1699 emission.select = False
1700 else:
1701 emission = emission_placeholder
1702 make_links.append((active.outputs[out_i], emission.inputs[0]))
1704 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
1705 if emission.outputs[0].links.__len__() > 0:
1706 if not emission.outputs[0].links[0].to_node == materialout:
1707 make_links.append((emission.outputs[0], materialout.inputs[0]))
1708 else:
1709 make_links.append((emission.outputs[0], materialout.inputs[0]))
1711 # Set brightness of viewer to compensate for Film and CM exposure
1712 intensity = 1/context.scene.cycles.film_exposure # Film exposure is a multiplier
1713 intensity /= pow(2, (context.scene.view_settings.exposure)) # CM exposure is measured in stops/EVs (2^x)
1714 emission.inputs[1].default_value = intensity
1716 else:
1717 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
1718 make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
1719 for node in nodes:
1720 if node.name == 'Emission Viewer':
1721 node.select = True
1722 bpy.ops.node.delete()
1723 for li_from, li_to in make_links:
1724 links.new(li_from, li_to)
1725 # Restore selection
1726 nodes.active = active
1727 for node in nodes:
1728 if node.name in selection:
1729 node.select = True
1730 force_update(context)
1731 return {'FINISHED'}
1732 else:
1733 return {'CANCELLED'}
1736 class NWFrameSelected(Operator, NWBase):
1737 bl_idname = "node.nw_frame_selected"
1738 bl_label = "Frame Selected"
1739 bl_description = "Add a frame node and parent the selected nodes to it"
1740 bl_options = {'REGISTER', 'UNDO'}
1741 label_prop = StringProperty(name='Label', default=' ', description='The visual name of the frame node')
1742 color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6),
1743 min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3)
1745 def execute(self, context):
1746 nodes, links = get_nodes_links(context)
1747 selected = []
1748 for node in nodes:
1749 if node.select == True:
1750 selected.append(node)
1752 bpy.ops.node.add_node(type='NodeFrame')
1753 frm = nodes.active
1754 frm.label = self.label_prop
1755 frm.use_custom_color = True
1756 frm.color = self.color_prop
1758 for node in selected:
1759 node.parent = frm
1761 return {'FINISHED'}
1764 class NWReloadImages(Operator, NWBase):
1765 bl_idname = "node.nw_reload_images"
1766 bl_label = "Reload Images"
1767 bl_description = "Update all the image nodes to match their files on disk"
1769 def execute(self, context):
1770 nodes, links = get_nodes_links(context)
1771 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1772 num_reloaded = 0
1773 for node in nodes:
1774 if node.type in image_types:
1775 if node.type == "TEXTURE":
1776 if node.texture: # node has texture assigned
1777 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1778 if node.texture.image: # texture has image assigned
1779 node.texture.image.reload()
1780 num_reloaded += 1
1781 else:
1782 if node.image:
1783 node.image.reload()
1784 num_reloaded += 1
1786 if num_reloaded:
1787 self.report({'INFO'}, "Reloaded images")
1788 print("Reloaded " + str(num_reloaded) + " images")
1789 force_update(context)
1790 return {'FINISHED'}
1791 else:
1792 self.report({'WARNING'}, "No images found to reload in this node tree")
1793 return {'CANCELLED'}
1796 class NWSwitchNodeType(Operator, NWBase):
1797 """Switch type of selected nodes """
1798 bl_idname = "node.nw_swtch_node_type"
1799 bl_label = "Switch Node Type"
1800 bl_options = {'REGISTER', 'UNDO'}
1802 to_type: EnumProperty(
1803 name="Switch to type",
1804 items=list(shaders_input_nodes_props) +
1805 list(shaders_output_nodes_props) +
1806 list(shaders_shader_nodes_props) +
1807 list(shaders_texture_nodes_props) +
1808 list(shaders_color_nodes_props) +
1809 list(shaders_vector_nodes_props) +
1810 list(shaders_converter_nodes_props) +
1811 list(shaders_layout_nodes_props) +
1812 list(compo_input_nodes_props) +
1813 list(compo_output_nodes_props) +
1814 list(compo_color_nodes_props) +
1815 list(compo_converter_nodes_props) +
1816 list(compo_filter_nodes_props) +
1817 list(compo_vector_nodes_props) +
1818 list(compo_matte_nodes_props) +
1819 list(compo_distort_nodes_props) +
1820 list(compo_layout_nodes_props) +
1821 list(blender_mat_input_nodes_props) +
1822 list(blender_mat_output_nodes_props) +
1823 list(blender_mat_color_nodes_props) +
1824 list(blender_mat_vector_nodes_props) +
1825 list(blender_mat_converter_nodes_props) +
1826 list(blender_mat_layout_nodes_props) +
1827 list(texture_input_nodes_props) +
1828 list(texture_output_nodes_props) +
1829 list(texture_color_nodes_props) +
1830 list(texture_pattern_nodes_props) +
1831 list(texture_textures_nodes_props) +
1832 list(texture_converter_nodes_props) +
1833 list(texture_distort_nodes_props) +
1834 list(texture_layout_nodes_props)
1837 def execute(self, context):
1838 nodes, links = get_nodes_links(context)
1839 to_type = self.to_type
1840 # Those types of nodes will not swap.
1841 src_excludes = ('NodeFrame')
1842 # Those attributes of nodes will be copied if possible
1843 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1844 'show_options', 'show_preview', 'show_texture',
1845 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1847 selected = [n for n in nodes if n.select]
1848 reselect = []
1849 for node in [n for n in selected if
1850 n.rna_type.identifier not in src_excludes and
1851 n.rna_type.identifier != to_type]:
1852 new_node = nodes.new(to_type)
1853 for attr in attrs_to_pass:
1854 if hasattr(node, attr) and hasattr(new_node, attr):
1855 setattr(new_node, attr, getattr(node, attr))
1856 # set image datablock of dst to image of src
1857 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1858 if node.image:
1859 new_node.image = node.image
1860 # Special cases
1861 if new_node.type == 'SWITCH':
1862 new_node.hide = True
1863 # Dictionaries: src_sockets and dst_sockets:
1864 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1865 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1866 # in 'INPUTS' and 'OUTPUTS':
1867 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1868 # socket entry:
1869 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1870 src_sockets = {
1871 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1872 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1874 dst_sockets = {
1875 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1876 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1878 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1879 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1880 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1881 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1882 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1883 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1884 # enumerate in inputs, then in outputs
1885 # find name, default value and links of socket
1886 for i, socket in enumerate(in_out):
1887 the_name = socket.name
1888 dval = None
1889 # Not every socket, especially in outputs has "default_value"
1890 if hasattr(socket, 'default_value'):
1891 dval = socket.default_value
1892 socket_links = []
1893 for lnk in socket.links:
1894 socket_links.append(lnk)
1895 # check type of socket to fill proper keys.
1896 for the_type in types_order_one:
1897 if socket.type == the_type:
1898 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1899 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1900 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1901 # Check which of the types in inputs/outputs is considered to be "main".
1902 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1903 for type_check in types_order_one:
1904 if sockets[in_out_name][type_check]:
1905 sockets[in_out_name]['MAIN'] = type_check
1906 break
1908 matches = {
1909 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1910 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1913 for inout, soctype in (
1914 ('INPUTS', 'MAIN',),
1915 ('INPUTS', 'SHADER',),
1916 ('INPUTS', 'RGBA',),
1917 ('INPUTS', 'VECTOR',),
1918 ('INPUTS', 'VALUE',),
1919 ('OUTPUTS', 'MAIN',),
1920 ('OUTPUTS', 'SHADER',),
1921 ('OUTPUTS', 'RGBA',),
1922 ('OUTPUTS', 'VECTOR',),
1923 ('OUTPUTS', 'VALUE',),
1925 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1926 if soctype == 'MAIN':
1927 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1928 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1929 else:
1930 sc = src_sockets[inout][soctype]
1931 dt = dst_sockets[inout][soctype]
1932 # start with 'dt' to determine number of possibilities.
1933 for i, soc in enumerate(dt):
1934 # if src main has enough entries - match them with dst main sockets by indexes.
1935 if len(sc) > i:
1936 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1937 # add 'VALUE_NAME' criterion to inputs.
1938 if inout == 'INPUTS' and soctype == 'VALUE':
1939 for s in sc:
1940 if s[2] == soc[2]: # if names match
1941 # append src (index, dval), dst (index, dval)
1942 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1944 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1945 # This creates better links when relinking textures.
1946 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1947 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1949 # Pass default values and RELINK:
1950 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1951 # INPUTS: Base on matches in proper order.
1952 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1953 # pass dvals
1954 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1955 new_node.inputs[dst_i].default_value = src_dval
1956 # Special case: switch to math
1957 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1958 new_node.type == 'MATH' and\
1959 tp == 'MAIN':
1960 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1961 new_node.inputs[dst_i].default_value = new_dst_dval
1962 if node.type == 'MIX_RGB':
1963 if node.blend_type in [o[0] for o in operations]:
1964 new_node.operation = node.blend_type
1965 # Special case: switch from math to some types
1966 if node.type == 'MATH' and\
1967 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1968 tp == 'MAIN':
1969 for i in range(3):
1970 new_node.inputs[dst_i].default_value[i] = src_dval
1971 if new_node.type == 'MIX_RGB':
1972 if node.operation in [t[0] for t in blend_types]:
1973 new_node.blend_type = node.operation
1974 # Set Fac of MIX_RGB to 1.0
1975 new_node.inputs[0].default_value = 1.0
1976 # make link only when dst matching input is not linked already.
1977 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1978 in_src_link = node.inputs[src_i].links[0]
1979 in_dst_socket = new_node.inputs[dst_i]
1980 links.new(in_src_link.from_socket, in_dst_socket)
1981 links.remove(in_src_link)
1982 # OUTPUTS: Base on matches in proper order.
1983 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1984 for out_src_link in node.outputs[src_i].links:
1985 out_dst_socket = new_node.outputs[dst_i]
1986 links.new(out_dst_socket, out_src_link.to_socket)
1987 # relink rest inputs if possible, no criteria
1988 for src_inp in node.inputs:
1989 for dst_inp in new_node.inputs:
1990 if src_inp.links and not dst_inp.links:
1991 src_link = src_inp.links[0]
1992 links.new(src_link.from_socket, dst_inp)
1993 links.remove(src_link)
1994 # relink rest outputs if possible, base on node kind if any left.
1995 for src_o in node.outputs:
1996 for out_src_link in src_o.links:
1997 for dst_o in new_node.outputs:
1998 if src_o.type == dst_o.type:
1999 links.new(dst_o, out_src_link.to_socket)
2000 # relink rest outputs no criteria if any left. Link all from first output.
2001 for src_o in node.outputs:
2002 for out_src_link in src_o.links:
2003 if new_node.outputs:
2004 links.new(new_node.outputs[0], out_src_link.to_socket)
2005 nodes.remove(node)
2006 force_update(context)
2007 return {'FINISHED'}
2010 class NWMergeNodes(Operator, NWBase):
2011 bl_idname = "node.nw_merge_nodes"
2012 bl_label = "Merge Nodes"
2013 bl_description = "Merge Selected Nodes"
2014 bl_options = {'REGISTER', 'UNDO'}
2016 mode: EnumProperty(
2017 name="mode",
2018 description="All possible blend types and math operations",
2019 items=blend_types + [op for op in operations if op not in blend_types],
2021 merge_type: EnumProperty(
2022 name="merge type",
2023 description="Type of Merge to be used",
2024 items=(
2025 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2026 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2027 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2028 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2029 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2030 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2034 def execute(self, context):
2035 settings = context.user_preferences.addons[__name__].preferences
2036 merge_hide = settings.merge_hide
2037 merge_position = settings.merge_position # 'center' or 'bottom'
2039 do_hide = False
2040 do_hide_shader = False
2041 if merge_hide == 'ALWAYS':
2042 do_hide = True
2043 do_hide_shader = True
2044 elif merge_hide == 'NON_SHADER':
2045 do_hide = True
2047 tree_type = context.space_data.node_tree.type
2048 if tree_type == 'COMPOSITING':
2049 node_type = 'CompositorNode'
2050 elif tree_type == 'SHADER':
2051 node_type = 'ShaderNode'
2052 elif tree_type == 'TEXTURE':
2053 node_type = 'TextureNode'
2054 nodes, links = get_nodes_links(context)
2055 mode = self.mode
2056 merge_type = self.merge_type
2057 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2058 # 'ZCOMBINE' works only if mode == 'MIX'
2059 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2060 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2061 merge_type = 'MIX'
2062 mode = 'MIX'
2063 selected_mix = [] # entry = [index, loc]
2064 selected_shader = [] # entry = [index, loc]
2065 selected_math = [] # entry = [index, loc]
2066 selected_z = [] # entry = [index, loc]
2067 selected_alphaover = [] # entry = [index, loc]
2069 for i, node in enumerate(nodes):
2070 if node.select and node.outputs:
2071 if merge_type == 'AUTO':
2072 for (type, types_list, dst) in (
2073 ('SHADER', ('MIX', 'ADD'), selected_shader),
2074 ('RGBA', [t[0] for t in blend_types], selected_mix),
2075 ('VALUE', [t[0] for t in operations], selected_math),
2077 output_type = node.outputs[0].type
2078 valid_mode = mode in types_list
2079 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2080 # Cheat that output type is 'RGBA',
2081 # and that 'MIX' exists in math operations list.
2082 # This way when selected_mix list is analyzed:
2083 # Node data will be appended even though it doesn't meet requirements.
2084 if output_type != 'SHADER' and mode == 'MIX':
2085 output_type = 'RGBA'
2086 valid_mode = True
2087 if output_type == type and valid_mode:
2088 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2089 else:
2090 for (type, types_list, dst) in (
2091 ('SHADER', ('MIX', 'ADD'), selected_shader),
2092 ('MIX', [t[0] for t in blend_types], selected_mix),
2093 ('MATH', [t[0] for t in operations], selected_math),
2094 ('ZCOMBINE', ('MIX', ), selected_z),
2095 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2097 if merge_type == type and mode in types_list:
2098 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2099 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2100 # use only 'Mix' nodes for merging.
2101 # For that we add selected_math list to selected_mix list and clear selected_math.
2102 if selected_mix and selected_math and merge_type == 'AUTO':
2103 selected_mix += selected_math
2104 selected_math = []
2106 for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
2107 if nodes_list:
2108 count_before = len(nodes)
2109 # sort list by loc_x - reversed
2110 nodes_list.sort(key=lambda k: k[1], reverse=True)
2111 # get maximum loc_x
2112 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2113 nodes_list.sort(key=lambda k: k[2], reverse=True)
2114 if merge_position == 'CENTER':
2115 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)
2116 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2117 if do_hide:
2118 loc_y += 40
2119 else:
2120 loc_y += 80
2121 else:
2122 loc_y = nodes_list[len(nodes_list) - 1][2]
2123 offset_y = 100
2124 if not do_hide:
2125 offset_y = 200
2126 if nodes_list == selected_shader and not do_hide_shader:
2127 offset_y = 150.0
2128 the_range = len(nodes_list) - 1
2129 if len(nodes_list) == 1:
2130 the_range = 1
2131 for i in range(the_range):
2132 if nodes_list == selected_mix:
2133 add_type = node_type + 'MixRGB'
2134 add = nodes.new(add_type)
2135 add.blend_type = mode
2136 if mode != 'MIX':
2137 add.inputs[0].default_value = 1.0
2138 add.show_preview = False
2139 add.hide = do_hide
2140 if do_hide:
2141 loc_y = loc_y - 50
2142 first = 1
2143 second = 2
2144 add.width_hidden = 100.0
2145 elif nodes_list == selected_math:
2146 add_type = node_type + 'Math'
2147 add = nodes.new(add_type)
2148 add.operation = mode
2149 add.hide = do_hide
2150 if do_hide:
2151 loc_y = loc_y - 50
2152 first = 0
2153 second = 1
2154 add.width_hidden = 100.0
2155 elif nodes_list == selected_shader:
2156 if mode == 'MIX':
2157 add_type = node_type + 'MixShader'
2158 add = nodes.new(add_type)
2159 add.hide = do_hide_shader
2160 if do_hide_shader:
2161 loc_y = loc_y - 50
2162 first = 1
2163 second = 2
2164 add.width_hidden = 100.0
2165 elif mode == 'ADD':
2166 add_type = node_type + 'AddShader'
2167 add = nodes.new(add_type)
2168 add.hide = do_hide_shader
2169 if do_hide_shader:
2170 loc_y = loc_y - 50
2171 first = 0
2172 second = 1
2173 add.width_hidden = 100.0
2174 elif nodes_list == selected_z:
2175 add = nodes.new('CompositorNodeZcombine')
2176 add.show_preview = False
2177 add.hide = do_hide
2178 if do_hide:
2179 loc_y = loc_y - 50
2180 first = 0
2181 second = 2
2182 add.width_hidden = 100.0
2183 elif nodes_list == selected_alphaover:
2184 add = nodes.new('CompositorNodeAlphaOver')
2185 add.show_preview = False
2186 add.hide = do_hide
2187 if do_hide:
2188 loc_y = loc_y - 50
2189 first = 1
2190 second = 2
2191 add.width_hidden = 100.0
2192 add.location = loc_x, loc_y
2193 loc_y += offset_y
2194 add.select = True
2195 count_adds = i + 1
2196 count_after = len(nodes)
2197 index = count_after - 1
2198 first_selected = nodes[nodes_list[0][0]]
2199 # "last" node has been added as first, so its index is count_before.
2200 last_add = nodes[count_before]
2201 # Special case:
2202 # Two nodes were selected and first selected has no output links, second selected has output links.
2203 # Then add links from last add to all links 'to_socket' of out links of second selected.
2204 if len(nodes_list) == 2:
2205 if not first_selected.outputs[0].links:
2206 second_selected = nodes[nodes_list[1][0]]
2207 for ss_link in second_selected.outputs[0].links:
2208 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2209 # Create list of invalid indexes.
2210 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2211 # Link only if "to_node" index not in invalid indexes list.
2212 if ss_link.to_node not in [nodes[i] for i in invalid_i]:
2213 links.new(last_add.outputs[0], ss_link.to_socket)
2214 # add links from last_add to all links 'to_socket' of out links of first selected.
2215 for fs_link in first_selected.outputs[0].links:
2216 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2217 # Create list of invalid indexes.
2218 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2219 # Link only if "to_node" index not in invalid indexes list.
2220 if fs_link.to_node not in [nodes[i] for i in invalid_i]:
2221 links.new(last_add.outputs[0], fs_link.to_socket)
2222 # add link from "first" selected and "first" add node
2223 node_to = nodes[count_after - 1]
2224 links.new(first_selected.outputs[0], node_to.inputs[first])
2225 if node_to.type == 'ZCOMBINE':
2226 for fs_out in first_selected.outputs:
2227 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
2228 links.new(fs_out, node_to.inputs[1])
2229 break
2230 # add links between added ADD nodes and between selected and ADD nodes
2231 for i in range(count_adds):
2232 if i < count_adds - 1:
2233 node_from = nodes[index]
2234 node_to = nodes[index - 1]
2235 node_to_input_i = first
2236 node_to_z_i = 1 # if z combine - link z to first z input
2237 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2238 if node_to.type == 'ZCOMBINE':
2239 for from_out in node_from.outputs:
2240 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2241 links.new(from_out, node_to.inputs[node_to_z_i])
2242 if len(nodes_list) > 1:
2243 node_from = nodes[nodes_list[i + 1][0]]
2244 node_to = nodes[index]
2245 node_to_input_i = second
2246 node_to_z_i = 3 # if z combine - link z to second z input
2247 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2248 if node_to.type == 'ZCOMBINE':
2249 for from_out in node_from.outputs:
2250 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2251 links.new(from_out, node_to.inputs[node_to_z_i])
2252 index -= 1
2253 # set "last" of added nodes as active
2254 nodes.active = last_add
2255 for i, x, y, dx, h in nodes_list:
2256 nodes[i].select = False
2258 return {'FINISHED'}
2261 class NWBatchChangeNodes(Operator, NWBase):
2262 bl_idname = "node.nw_batch_change"
2263 bl_label = "Batch Change"
2264 bl_description = "Batch Change Blend Type and Math Operation"
2265 bl_options = {'REGISTER', 'UNDO'}
2267 blend_type: EnumProperty(
2268 name="Blend Type",
2269 items=blend_types + navs,
2271 operation: EnumProperty(
2272 name="Operation",
2273 items=operations + navs,
2276 def execute(self, context):
2278 nodes, links = get_nodes_links(context)
2279 blend_type = self.blend_type
2280 operation = self.operation
2281 for node in context.selected_nodes:
2282 if node.type == 'MIX_RGB':
2283 if not blend_type in [nav[0] for nav in navs]:
2284 node.blend_type = blend_type
2285 else:
2286 if blend_type == 'NEXT':
2287 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2288 #index = blend_types.index(node.blend_type)
2289 if index == len(blend_types) - 1:
2290 node.blend_type = blend_types[0][0]
2291 else:
2292 node.blend_type = blend_types[index + 1][0]
2294 if blend_type == 'PREV':
2295 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2296 if index == 0:
2297 node.blend_type = blend_types[len(blend_types) - 1][0]
2298 else:
2299 node.blend_type = blend_types[index - 1][0]
2301 if node.type == 'MATH':
2302 if not operation in [nav[0] for nav in navs]:
2303 node.operation = operation
2304 else:
2305 if operation == 'NEXT':
2306 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2307 #index = operations.index(node.operation)
2308 if index == len(operations) - 1:
2309 node.operation = operations[0][0]
2310 else:
2311 node.operation = operations[index + 1][0]
2313 if operation == 'PREV':
2314 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2315 #index = operations.index(node.operation)
2316 if index == 0:
2317 node.operation = operations[len(operations) - 1][0]
2318 else:
2319 node.operation = operations[index - 1][0]
2321 return {'FINISHED'}
2324 class NWChangeMixFactor(Operator, NWBase):
2325 bl_idname = "node.nw_factor"
2326 bl_label = "Change Factor"
2327 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2328 bl_options = {'REGISTER', 'UNDO'}
2330 # option: Change factor.
2331 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2332 # Else - change factor by option value.
2333 option: FloatProperty()
2335 def execute(self, context):
2336 nodes, links = get_nodes_links(context)
2337 option = self.option
2338 selected = [] # entry = index
2339 for si, node in enumerate(nodes):
2340 if node.select:
2341 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2342 selected.append(si)
2344 for si in selected:
2345 fac = nodes[si].inputs[0]
2346 nodes[si].hide = False
2347 if option in {0.0, 1.0}:
2348 fac.default_value = option
2349 else:
2350 fac.default_value += option
2352 return {'FINISHED'}
2355 class NWCopySettings(Operator, NWBase):
2356 bl_idname = "node.nw_copy_settings"
2357 bl_label = "Copy Settings"
2358 bl_description = "Copy Settings of Active Node to Selected Nodes"
2359 bl_options = {'REGISTER', 'UNDO'}
2361 @classmethod
2362 def poll(cls, context):
2363 valid = False
2364 if nw_check(context):
2365 if context.active_node is not None and context.active_node.type is not 'FRAME':
2366 valid = True
2367 return valid
2369 def execute(self, context):
2370 node_active = context.active_node
2371 node_selected = context.selected_nodes
2373 # Error handling
2374 if not (len(node_selected) > 1):
2375 self.report({'ERROR'}, "2 nodes must be selected at least")
2376 return {'CANCELLED'}
2378 # Check if active node is in the selection
2379 selected_node_names = [n.name for n in node_selected]
2380 if node_active.name not in selected_node_names:
2381 self.report({'ERROR'}, "No active node")
2382 return {'CANCELLED'}
2384 # Get nodes in selection by type
2385 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2387 if not (len(valid_nodes) > 1) and node_active:
2388 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2389 return {'CANCELLED'}
2391 if len(valid_nodes) != len(node_selected):
2392 # Report nodes that are not valid
2393 valid_node_names = [n.name for n in valid_nodes]
2394 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2395 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2397 # Reference original
2398 orig = node_active
2399 #node_selected_names = [n.name for n in node_selected]
2401 # Output list
2402 success_names = []
2404 # Deselect all nodes
2405 for i in node_selected:
2406 i.select = False
2408 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2409 # Run through all other nodes
2410 for node in valid_nodes[1:]:
2412 # Check for frame node
2413 parent = node.parent if node.parent else None
2414 node_loc = [node.location.x, node.location.y]
2416 # Select original to duplicate
2417 orig.select = True
2419 # Duplicate selected node
2420 bpy.ops.node.duplicate()
2421 new_node = context.selected_nodes[0]
2423 # Deselect copy
2424 new_node.select = False
2426 # Properties to copy
2427 node_tree = node.id_data
2428 props_to_copy = 'bl_idname name location height width'.split(' ')
2430 # Input and outputs
2431 reconnections = []
2432 mappings = chain.from_iterable([node.inputs, node.outputs])
2433 for i in (i for i in mappings if i.is_linked):
2434 for L in i.links:
2435 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2437 # Properties
2438 props = {j: getattr(node, j) for j in props_to_copy}
2439 props_to_copy.pop(0)
2441 for prop in props_to_copy:
2442 setattr(new_node, prop, props[prop])
2444 # Get the node tree to remove the old node
2445 nodes = node_tree.nodes
2446 nodes.remove(node)
2447 new_node.name = props['name']
2449 if parent:
2450 new_node.parent = parent
2451 new_node.location = node_loc
2453 for str_from, str_to in reconnections:
2454 node_tree.links.new(eval(str_from), eval(str_to))
2456 success_names.append(new_node.name)
2458 orig.select = True
2459 node_tree.nodes.active = orig
2460 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2461 return {'FINISHED'}
2464 class NWCopyLabel(Operator, NWBase):
2465 bl_idname = "node.nw_copy_label"
2466 bl_label = "Copy Label"
2467 bl_options = {'REGISTER', 'UNDO'}
2469 option: EnumProperty(
2470 name="option",
2471 description="Source of name of label",
2472 items=(
2473 ('FROM_ACTIVE', 'from active', 'from active node',),
2474 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2475 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2479 def execute(self, context):
2480 nodes, links = get_nodes_links(context)
2481 option = self.option
2482 active = nodes.active
2483 if option == 'FROM_ACTIVE':
2484 if active:
2485 src_label = active.label
2486 for node in [n for n in nodes if n.select and nodes.active != n]:
2487 node.label = src_label
2488 elif option == 'FROM_NODE':
2489 selected = [n for n in nodes if n.select]
2490 for node in selected:
2491 for input in node.inputs:
2492 if input.links:
2493 src = input.links[0].from_node
2494 node.label = src.label
2495 break
2496 elif option == 'FROM_SOCKET':
2497 selected = [n for n in nodes if n.select]
2498 for node in selected:
2499 for input in node.inputs:
2500 if input.links:
2501 src = input.links[0].from_socket
2502 node.label = src.name
2503 break
2505 return {'FINISHED'}
2508 class NWClearLabel(Operator, NWBase):
2509 bl_idname = "node.nw_clear_label"
2510 bl_label = "Clear Label"
2511 bl_options = {'REGISTER', 'UNDO'}
2513 option: BoolProperty()
2515 def execute(self, context):
2516 nodes, links = get_nodes_links(context)
2517 for node in [n for n in nodes if n.select]:
2518 node.label = ''
2520 return {'FINISHED'}
2522 def invoke(self, context, event):
2523 if self.option:
2524 return self.execute(context)
2525 else:
2526 return context.window_manager.invoke_confirm(self, event)
2529 class NWModifyLabels(Operator, NWBase):
2530 """Modify Labels of all selected nodes"""
2531 bl_idname = "node.nw_modify_labels"
2532 bl_label = "Modify Labels"
2533 bl_options = {'REGISTER', 'UNDO'}
2535 prepend = StringProperty(
2536 name="Add to Beginning"
2538 append = StringProperty(
2539 name="Add to End"
2541 replace_from = StringProperty(
2542 name="Text to Replace"
2544 replace_to = StringProperty(
2545 name="Replace with"
2548 def execute(self, context):
2549 nodes, links = get_nodes_links(context)
2550 for node in [n for n in nodes if n.select]:
2551 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2553 return {'FINISHED'}
2555 def invoke(self, context, event):
2556 self.prepend = ""
2557 self.append = ""
2558 self.remove = ""
2559 return context.window_manager.invoke_props_dialog(self)
2562 class NWAddTextureSetup(Operator, NWBase):
2563 bl_idname = "node.nw_add_texture"
2564 bl_label = "Texture Setup"
2565 bl_description = "Add Texture Node Setup to Selected Shaders"
2566 bl_options = {'REGISTER', 'UNDO'}
2568 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2570 @classmethod
2571 def poll(cls, context):
2572 valid = False
2573 if nw_check(context):
2574 space = context.space_data
2575 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2576 valid = True
2577 return valid
2579 def execute(self, context):
2580 nodes, links = get_nodes_links(context)
2581 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2582 texture_types = [x[1] for x in shaders_texture_nodes_props]
2583 selected_nodes = [n for n in nodes if n.select]
2584 for t_node in selected_nodes:
2585 valid = False
2586 input_index = 0
2587 if t_node.inputs:
2588 for index, i in enumerate(t_node.inputs):
2589 if not i.is_linked:
2590 valid = True
2591 input_index = index
2592 break
2593 if valid:
2594 locx = t_node.location.x
2595 locy = t_node.location.y - t_node.dimensions.y/2
2597 xoffset = [500, 700]
2598 is_texture = False
2599 if t_node.type in texture_types + ['MAPPING']:
2600 xoffset = [290, 500]
2601 is_texture = True
2603 coordout = 2
2604 image_type = 'ShaderNodeTexImage'
2606 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
2607 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2608 if t_node.type == 'BACKGROUND':
2609 image_type = 'ShaderNodeTexEnvironment'
2611 if not is_texture:
2612 tex = nodes.new(image_type)
2613 tex.location = [locx - 200, locy + 112]
2614 nodes.active = tex
2615 links.new(tex.outputs[0], t_node.inputs[input_index])
2617 t_node.select = False
2618 if self.add_mapping or is_texture:
2619 if t_node.type != 'MAPPING':
2620 m = nodes.new('ShaderNodeMapping')
2621 m.location = [locx - xoffset[0], locy + 141]
2622 m.width = 240
2623 else:
2624 m = t_node
2625 coord = nodes.new('ShaderNodeTexCoord')
2626 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
2628 if not is_texture:
2629 links.new(m.outputs[0], tex.inputs[0])
2630 links.new(coord.outputs[coordout], m.inputs[0])
2631 else:
2632 nodes.active = m
2633 links.new(m.outputs[0], t_node.inputs[input_index])
2634 links.new(coord.outputs[coordout], m.inputs[0])
2635 else:
2636 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
2637 return {'FINISHED'}
2640 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2641 bl_idname = "node.nw_add_textures_for_principled"
2642 bl_label = "Principled Texture Setup"
2643 bl_description = "Add Texture Node Setup for Principled BSDF"
2644 bl_options = {'REGISTER', 'UNDO'}
2646 directory = StringProperty(
2647 name='Directory',
2648 subtype='DIR_PATH',
2649 default='',
2650 description='Folder to search in for image files')
2651 files = CollectionProperty(
2652 type=bpy.types.OperatorFileListElement,
2653 options={'HIDDEN', 'SKIP_SAVE'})
2655 order = [
2656 "filepath",
2657 "files",
2660 @classmethod
2661 def poll(cls, context):
2662 valid = False
2663 if nw_check(context):
2664 space = context.space_data
2665 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2666 valid = True
2667 return valid
2669 def execute(self, context):
2670 # Check if everything is ok
2671 if not self.directory:
2672 self.report({'INFO'}, 'No Folder Selected')
2673 return {'CANCELLED'}
2674 if not self.files[:]:
2675 self.report({'INFO'}, 'No Files Selected')
2676 return {'CANCELLED'}
2678 nodes, links = get_nodes_links(context)
2679 active_node = nodes.active
2680 if not active_node.bl_idname == 'ShaderNodeBsdfPrincipled':
2681 self.report({'INFO'}, 'Select Principled BSDF')
2682 return {'CANCELLED'}
2684 # Helper_functions
2685 def split_into__components(fname):
2686 # Split filename into components
2687 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2688 # Remove extension
2689 fname = path.splitext(fname)[0]
2690 # Remove digits
2691 fname = ''.join(i for i in fname if not i.isdigit())
2692 # Seperate CamelCase by space
2693 fname = re.sub("([a-z])([A-Z])","\g<1> \g<2>",fname)
2694 # Replace common separators with SPACE
2695 seperators = ['_', '.', '-', '__', '--', '#']
2696 for sep in seperators:
2697 fname = fname.replace(sep, ' ')
2699 components = fname.split(' ')
2700 components = [c.lower() for c in components]
2701 return components
2703 # Filter textures names for texturetypes in filenames
2704 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2705 tags = context.user_preferences.addons[__name__].preferences.principled_tags
2706 normal_abbr = tags.normal.split(' ')
2707 bump_abbr = tags.bump.split(' ')
2708 gloss_abbr = tags.gloss.split(' ')
2709 rough_abbr = tags.rough.split(' ')
2710 socketnames = [
2711 ['Displacement', tags.displacement.split(' '), None],
2712 ['Base Color', tags.base_color.split(' '), None],
2713 ['Subsurface Color', tags.sss_color.split(' '), None],
2714 ['Metallic', tags.metallic.split(' '), None],
2715 ['Specular', tags.specular.split(' '), None],
2716 ['Roughness', rough_abbr + gloss_abbr, None],
2717 ['Normal', normal_abbr + bump_abbr, None],
2720 # Look through texture_types and set value as filename of first matched file
2721 def match_files_to_socket_names():
2722 for sname in socketnames:
2723 for file in self.files:
2724 fname = file.name
2725 filenamecomponents = split_into__components(fname)
2726 matches = set(sname[1]).intersection(set(filenamecomponents))
2727 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2728 if matches:
2729 sname[2] = fname
2730 break
2732 match_files_to_socket_names()
2733 # Remove socketnames without found files
2734 socketnames = [s for s in socketnames if s[2]
2735 and path.exists(self.directory+s[2])]
2736 if not socketnames:
2737 self.report({'INFO'}, 'No matching images found')
2738 print('No matching images found')
2739 return {'CANCELLED'}
2741 # Add found images
2742 print('\nMatched Textures:')
2743 texture_nodes = []
2744 disp_texture = None
2745 normal_node = None
2746 roughness_node = None
2747 for i, sname in enumerate(socketnames):
2748 print(i, sname[0], sname[2])
2750 # DISPLACEMENT NODES
2751 if sname[0] == 'Displacement':
2752 disp_texture = nodes.new(type='ShaderNodeTexImage')
2753 img = bpy.data.images.load(self.directory+sname[2])
2754 disp_texture.image = img
2755 disp_texture.label = 'Displacement'
2756 disp_texture.color_space = 'NONE'
2758 # Add displacement offset nodes
2759 math_sub = nodes.new(type='ShaderNodeMath')
2760 math_sub.operation = 'SUBTRACT'
2761 math_sub.label = 'Offset'
2762 math_sub.location = active_node.location + Vector((0, -560))
2763 math_mul = nodes.new(type='ShaderNodeMath')
2764 math_mul.operation = 'MULTIPLY'
2765 math_mul.label = 'Strength'
2766 math_mul.location = math_sub.location + Vector((200, 0))
2767 link = links.new(math_mul.inputs[0], math_sub.outputs[0])
2768 link = links.new(math_sub.inputs[0], disp_texture.outputs[0])
2770 # Turn on true displacement in the material
2771 # Too complicated for now
2774 # Frame. Does not update immediatly
2775 # Seems to need an editor redraw
2776 frame = nodes.new(type='NodeFrame')
2777 frame.label = 'Displacement'
2778 math_sub.parent = frame
2779 math_mul.parent = frame
2780 frame.update()
2783 #find ouput node
2784 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2785 if output_node:
2786 if not output_node[0].inputs[2].is_linked:
2787 link = links.new(output_node[0].inputs[2], math_mul.outputs[0])
2789 continue
2791 if not active_node.inputs[sname[0]].is_linked:
2792 # No texture node connected -> add texture node with new image
2793 texture_node = nodes.new(type='ShaderNodeTexImage')
2794 img = bpy.data.images.load(self.directory+sname[2])
2795 texture_node.image = img
2797 # NORMAL NODES
2798 if sname[0] == 'Normal':
2799 # Test if new texture node is normal or bump map
2800 fname_components = split_into__components(sname[2])
2801 match_normal = set(normal_abbr).intersection(set(fname_components))
2802 match_bump = set(bump_abbr).intersection(set(fname_components))
2803 if match_normal:
2804 # If Normal add normal node in between
2805 normal_node = nodes.new(type='ShaderNodeNormalMap')
2806 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
2807 elif match_bump:
2808 # If Bump add bump node in between
2809 normal_node = nodes.new(type='ShaderNodeBump')
2810 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
2812 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
2813 normal_node_texture = texture_node
2815 elif sname[0] == 'Roughness':
2816 # Test if glossy or roughness map
2817 fname_components = split_into__components(sname[2])
2818 match_rough = set(rough_abbr).intersection(set(fname_components))
2819 match_gloss = set(gloss_abbr).intersection(set(fname_components))
2821 if match_rough:
2822 # If Roughness nothing to to
2823 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2825 elif match_gloss:
2826 # If Gloss Map add invert node
2827 invert_node = nodes.new(type='ShaderNodeInvert')
2828 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
2830 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
2831 roughness_node = texture_node
2833 else:
2834 # This is a simple connection Texture --> Input slot
2835 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2837 # Use non-color for all but 'Base Color' Textures
2838 if not sname[0] in ['Base Color']:
2839 texture_node.color_space = 'NONE'
2841 else:
2842 # If already texture connected. add to node list for alignment
2843 texture_node = active_node.inputs[sname[0]].links[0].from_node
2845 # This are all connected texture nodes
2846 texture_nodes.append(texture_node)
2847 texture_node.label = sname[0]
2849 if disp_texture:
2850 texture_nodes.append(disp_texture)
2852 # Alignment
2853 for i, texture_node in enumerate(texture_nodes):
2854 offset = Vector((-400, (i * -260) + 200))
2855 texture_node.location = active_node.location + offset
2857 if normal_node:
2858 # Extra alignment if normal node was added
2859 normal_node.location = normal_node_texture.location + Vector((200, 0))
2861 if roughness_node:
2862 # Alignment of invert node if glossy map
2863 invert_node.location = roughness_node.location + Vector((200, 0))
2865 # Add texture input + mapping
2866 mapping = nodes.new(type='ShaderNodeMapping')
2867 mapping.location = active_node.location + Vector((-900, 0))
2868 if len(texture_nodes) > 1:
2869 # If more than one texture add reroute node in between
2870 reroute = nodes.new(type='NodeReroute')
2871 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
2872 reroute.location = tex_coords + Vector((-50, -120))
2873 for texture_node in texture_nodes:
2874 link = links.new(texture_node.inputs[0], reroute.outputs[0])
2875 link = links.new(reroute.inputs[0], mapping.outputs[0])
2876 else:
2877 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
2879 # Connect texture_coordiantes to mapping node
2880 texture_input = nodes.new(type='ShaderNodeTexCoord')
2881 texture_input.location = mapping.location + Vector((-200, 0))
2882 link = links.new(mapping.inputs[0], texture_input.outputs[2])
2884 # Just to be sure
2885 active_node.select = False
2886 nodes.update()
2887 links.update()
2888 force_update(context)
2889 return {'FINISHED'}
2892 class NWAddReroutes(Operator, NWBase):
2893 """Add Reroute Nodes and link them to outputs of selected nodes"""
2894 bl_idname = "node.nw_add_reroutes"
2895 bl_label = "Add Reroutes"
2896 bl_description = "Add Reroutes to Outputs"
2897 bl_options = {'REGISTER', 'UNDO'}
2899 option: EnumProperty(
2900 name="option",
2901 items=[
2902 ('ALL', 'to all', 'Add to all outputs'),
2903 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2904 ('LINKED', 'to linked', 'Add only to linked outputs'),
2908 def execute(self, context):
2909 tree_type = context.space_data.node_tree.type
2910 option = self.option
2911 nodes, links = get_nodes_links(context)
2912 # output valid when option is 'all' or when 'loose' output has no links
2913 valid = False
2914 post_select = [] # nodes to be selected after execution
2915 # create reroutes and recreate links
2916 for node in [n for n in nodes if n.select]:
2917 if node.outputs:
2918 x = node.location.x
2919 y = node.location.y
2920 width = node.width
2921 # unhide 'REROUTE' nodes to avoid issues with location.y
2922 if node.type == 'REROUTE':
2923 node.hide = False
2924 # When node is hidden - width_hidden not usable.
2925 # Hack needed to calculate real width
2926 if node.hide:
2927 bpy.ops.node.select_all(action='DESELECT')
2928 helper = nodes.new('NodeReroute')
2929 helper.select = True
2930 node.select = True
2931 # resize node and helper to zero. Then check locations to calculate width
2932 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2933 width = 2.0 * (helper.location.x - node.location.x)
2934 # restore node location
2935 node.location = x, y
2936 # delete helper
2937 node.select = False
2938 # only helper is selected now
2939 bpy.ops.node.delete()
2940 x = node.location.x + width + 20.0
2941 if node.type != 'REROUTE':
2942 y -= 35.0
2943 y_offset = -22.0
2944 loc = x, y
2945 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2946 for out_i, output in enumerate(node.outputs):
2947 pass_used = False # initial value to be analyzed if 'R_LAYERS'
2948 # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
2949 if node.type != 'R_LAYERS':
2950 pass_used = True
2951 else: # if 'R_LAYERS' check if output represent used render pass
2952 node_scene = node.scene
2953 node_layer = node.layer
2954 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2955 if output.name == 'Alpha':
2956 pass_used = True
2957 else:
2958 # check entries in global 'rl_outputs' variable
2959 #for render_pass, output_name, exr_name, in_internal, in_cycles in rl_outputs:
2960 for rlo in rl_outputs:
2961 if output.name == rlo.output_name or output.name == rlo.exr_output_name:
2962 pass_used = getattr(node_scene.render.layers[node_layer], rlo.render_pass)
2963 break
2964 if pass_used:
2965 valid = ((option == 'ALL') or
2966 (option == 'LOOSE' and not output.links) or
2967 (option == 'LINKED' and output.links))
2968 # Add reroutes only if valid, but offset location in all cases.
2969 if valid:
2970 n = nodes.new('NodeReroute')
2971 nodes.active = n
2972 for link in output.links:
2973 links.new(n.outputs[0], link.to_socket)
2974 links.new(output, n.inputs[0])
2975 n.location = loc
2976 post_select.append(n)
2977 reroutes_count += 1
2978 y += y_offset
2979 loc = x, y
2980 # disselect the node so that after execution of script only newly created nodes are selected
2981 node.select = False
2982 # nicer reroutes distribution along y when node.hide
2983 if node.hide:
2984 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2985 for reroute in [r for r in nodes if r.select]:
2986 reroute.location.y -= y_translate
2987 for node in post_select:
2988 node.select = True
2990 return {'FINISHED'}
2993 class NWLinkActiveToSelected(Operator, NWBase):
2994 """Link active node to selected nodes basing on various criteria"""
2995 bl_idname = "node.nw_link_active_to_selected"
2996 bl_label = "Link Active Node to Selected"
2997 bl_options = {'REGISTER', 'UNDO'}
2999 replace: BoolProperty()
3000 use_node_name: BoolProperty()
3001 use_outputs_names: BoolProperty()
3003 @classmethod
3004 def poll(cls, context):
3005 valid = False
3006 if nw_check(context):
3007 if context.active_node is not None:
3008 if context.active_node.select:
3009 valid = True
3010 return valid
3012 def execute(self, context):
3013 nodes, links = get_nodes_links(context)
3014 replace = self.replace
3015 use_node_name = self.use_node_name
3016 use_outputs_names = self.use_outputs_names
3017 active = nodes.active
3018 selected = [node for node in nodes if node.select and node != active]
3019 outputs = [] # Only usable outputs of active nodes will be stored here.
3020 for out in active.outputs:
3021 if active.type != 'R_LAYERS':
3022 outputs.append(out)
3023 else:
3024 # 'R_LAYERS' node type needs special handling.
3025 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3026 # Only outputs that represent used passes should be taken into account
3027 # Check if pass represented by output is used.
3028 # global 'rl_outputs' list will be used for that
3029 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
3030 pass_used = False # initial value. Will be set to True if pass is used
3031 if out.name == 'Alpha':
3032 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3033 pass_used = True
3034 elif out.name == out_name:
3035 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3036 pass_used = getattr(active.scene.render.layers[active.layer], render_pass)
3037 break
3038 if pass_used:
3039 outputs.append(out)
3040 doit = True # Will be changed to False when links successfully added to previous output.
3041 for out in outputs:
3042 if doit:
3043 for node in selected:
3044 dst_name = node.name # Will be compared with src_name if needed.
3045 # When node has label - use it as dst_name
3046 if node.label:
3047 dst_name = node.label
3048 valid = True # Initial value. Will be changed to False if names don't match.
3049 src_name = dst_name # If names not used - this asignment will keep valid = True.
3050 if use_node_name:
3051 # Set src_name to source node name or label
3052 src_name = active.name
3053 if active.label:
3054 src_name = active.label
3055 elif use_outputs_names:
3056 src_name = (out.name, )
3057 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
3058 if out.name in {out_name, exr_name}:
3059 src_name = (out_name, exr_name)
3060 if dst_name not in src_name:
3061 valid = False
3062 if valid:
3063 for input in node.inputs:
3064 if input.type == out.type or node.type == 'REROUTE':
3065 if replace or not input.is_linked:
3066 links.new(out, input)
3067 if not use_node_name and not use_outputs_names:
3068 doit = False
3069 break
3071 return {'FINISHED'}
3074 class NWAlignNodes(Operator, NWBase):
3075 '''Align the selected nodes neatly in a row/column'''
3076 bl_idname = "node.nw_align_nodes"
3077 bl_label = "Align Nodes"
3078 bl_options = {'REGISTER', 'UNDO'}
3079 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3081 def execute(self, context):
3082 nodes, links = get_nodes_links(context)
3083 margin = self.margin
3085 selection = []
3086 for node in nodes:
3087 if node.select and node.type != 'FRAME':
3088 selection.append(node)
3090 # If no nodes are selected, align all nodes
3091 active_loc = None
3092 if not selection:
3093 selection = nodes
3094 elif nodes.active in selection:
3095 active_loc = copy(nodes.active.location) # make a copy, not a reference
3097 # Check if nodes should be layed out horizontally or vertically
3098 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3099 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3100 x_range = max(x_locs) - min(x_locs)
3101 y_range = max(y_locs) - min(y_locs)
3102 mid_x = (max(x_locs) + min(x_locs)) / 2
3103 mid_y = (max(y_locs) + min(y_locs)) / 2
3104 horizontal = x_range > y_range
3106 # Sort selection by location of node mid-point
3107 if horizontal:
3108 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3109 else:
3110 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3112 # Alignment
3113 current_pos = 0
3114 for node in selection:
3115 current_margin = margin
3116 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3118 if horizontal:
3119 node.location.x = current_pos
3120 current_pos += current_margin + node.dimensions.x
3121 node.location.y = mid_y + (node.dimensions.y / 2)
3122 else:
3123 node.location.y = current_pos
3124 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3125 node.location.x = mid_x - (node.dimensions.x / 2)
3127 # If active node is selected, center nodes around it
3128 if active_loc is not None:
3129 active_loc_diff = active_loc - nodes.active.location
3130 for node in selection:
3131 node.location += active_loc_diff
3132 else: # Position nodes centered around where they used to be
3133 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
3134 new_mid = (max(locs) + min(locs)) / 2
3135 for node in selection:
3136 if horizontal:
3137 node.location.x += (mid_x - new_mid)
3138 else:
3139 node.location.y += (mid_y - new_mid)
3141 return {'FINISHED'}
3144 class NWSelectParentChildren(Operator, NWBase):
3145 bl_idname = "node.nw_select_parent_child"
3146 bl_label = "Select Parent or Children"
3147 bl_options = {'REGISTER', 'UNDO'}
3149 option: EnumProperty(
3150 name="option",
3151 items=(
3152 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3153 ('CHILD', 'Select Children', 'Select members of selected frame'),
3157 def execute(self, context):
3158 nodes, links = get_nodes_links(context)
3159 option = self.option
3160 selected = [node for node in nodes if node.select]
3161 if option == 'PARENT':
3162 for sel in selected:
3163 parent = sel.parent
3164 if parent:
3165 parent.select = True
3166 else: # option == 'CHILD'
3167 for sel in selected:
3168 children = [node for node in nodes if node.parent == sel]
3169 for kid in children:
3170 kid.select = True
3172 return {'FINISHED'}
3175 class NWDetachOutputs(Operator, NWBase):
3176 """Detach outputs of selected node leaving inputs linked"""
3177 bl_idname = "node.nw_detach_outputs"
3178 bl_label = "Detach Outputs"
3179 bl_options = {'REGISTER', 'UNDO'}
3181 def execute(self, context):
3182 nodes, links = get_nodes_links(context)
3183 selected = context.selected_nodes
3184 bpy.ops.node.duplicate_move_keep_inputs()
3185 new_nodes = context.selected_nodes
3186 bpy.ops.node.select_all(action="DESELECT")
3187 for node in selected:
3188 node.select = True
3189 bpy.ops.node.delete_reconnect()
3190 for new_node in new_nodes:
3191 new_node.select = True
3192 bpy.ops.transform.translate('INVOKE_DEFAULT')
3194 return {'FINISHED'}
3197 class NWLinkToOutputNode(Operator, NWBase):
3198 """Link to Composite node or Material Output node"""
3199 bl_idname = "node.nw_link_out"
3200 bl_label = "Connect to Output"
3201 bl_options = {'REGISTER', 'UNDO'}
3203 @classmethod
3204 def poll(cls, context):
3205 valid = False
3206 if nw_check(context):
3207 if context.active_node is not None:
3208 for out in context.active_node.outputs:
3209 if not out.hide:
3210 valid = True
3211 break
3212 return valid
3214 def execute(self, context):
3215 nodes, links = get_nodes_links(context)
3216 active = nodes.active
3217 output_node = None
3218 output_index = None
3219 tree_type = context.space_data.tree_type
3220 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
3221 output_types_compo = ['COMPOSITE']
3222 output_types_blender_mat = ['OUTPUT']
3223 output_types_textures = ['OUTPUT']
3224 output_types = output_types_shaders + output_types_compo + output_types_blender_mat
3225 for node in nodes:
3226 if node.type in output_types:
3227 output_node = node
3228 break
3229 if not output_node:
3230 bpy.ops.node.select_all(action="DESELECT")
3231 if tree_type == 'ShaderNodeTree':
3232 if is_cycles_or_eevee(context):
3233 output_node = nodes.new('ShaderNodeOutputMaterial')
3234 else:
3235 output_node = nodes.new('ShaderNodeOutput')
3236 elif tree_type == 'CompositorNodeTree':
3237 output_node = nodes.new('CompositorNodeComposite')
3238 elif tree_type == 'TextureNodeTree':
3239 output_node = nodes.new('TextureNodeOutput')
3240 output_node.location.x = active.location.x + active.dimensions.x + 80
3241 output_node.location.y = active.location.y
3242 if (output_node and active.outputs):
3243 for i, output in enumerate(active.outputs):
3244 if not output.hide:
3245 output_index = i
3246 break
3247 for i, output in enumerate(active.outputs):
3248 if output.type == output_node.inputs[0].type and not output.hide:
3249 output_index = i
3250 break
3252 out_input_index = 0
3253 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3254 if active.outputs[output_index].name == 'Volume':
3255 out_input_index = 1
3256 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3257 out_input_index = 2
3258 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3260 force_update(context) # viewport render does not update
3262 return {'FINISHED'}
3265 class NWMakeLink(Operator, NWBase):
3266 """Make a link from one socket to another"""
3267 bl_idname = 'node.nw_make_link'
3268 bl_label = 'Make Link'
3269 bl_options = {'REGISTER', 'UNDO'}
3270 from_socket: IntProperty()
3271 to_socket: IntProperty()
3273 def execute(self, context):
3274 nodes, links = get_nodes_links(context)
3276 n1 = nodes[context.scene.NWLazySource]
3277 n2 = nodes[context.scene.NWLazyTarget]
3279 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3281 force_update(context)
3283 return {'FINISHED'}
3286 class NWCallInputsMenu(Operator, NWBase):
3287 """Link from this output"""
3288 bl_idname = 'node.nw_call_inputs_menu'
3289 bl_label = 'Make Link'
3290 bl_options = {'REGISTER', 'UNDO'}
3291 from_socket: IntProperty()
3293 def execute(self, context):
3294 nodes, links = get_nodes_links(context)
3296 context.scene.NWSourceSocket = self.from_socket
3298 n1 = nodes[context.scene.NWLazySource]
3299 n2 = nodes[context.scene.NWLazyTarget]
3300 if len(n2.inputs) > 1:
3301 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3302 elif len(n2.inputs) == 1:
3303 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3304 return {'FINISHED'}
3307 class NWAddSequence(Operator, ImportHelper):
3308 """Add an Image Sequence"""
3309 bl_idname = 'node.nw_add_sequence'
3310 bl_label = 'Import Image Sequence'
3311 bl_options = {'REGISTER', 'UNDO'}
3312 directory = StringProperty(subtype="DIR_PATH")
3313 filename = StringProperty(subtype="FILE_NAME")
3314 files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
3316 def execute(self, context):
3317 nodes, links = get_nodes_links(context)
3318 directory = self.directory
3319 filename = self.filename
3320 files = self.files
3321 tree = context.space_data.node_tree
3323 # DEBUG
3324 # print ("\nDIR:", directory)
3325 # print ("FN:", filename)
3326 # print ("Fs:", list(f.name for f in files), '\n')
3328 if tree.type == 'SHADER':
3329 node_type = "ShaderNodeTexImage"
3330 elif tree.type == 'COMPOSITING':
3331 node_type = "CompositorNodeImage"
3332 else:
3333 self.report({'ERROR'}, "Unsupported Node Tree type!")
3334 return {'CANCELLED'}
3336 if not files[0].name and not filename:
3337 self.report({'ERROR'}, "No file chosen")
3338 return {'CANCELLED'}
3339 elif files[0].name and (not filename or not path.exists(directory+filename)):
3340 # User has selected multiple files without an active one, or the active one is non-existant
3341 filename = files[0].name
3343 if not path.exists(directory+filename):
3344 self.report({'ERROR'}, filename+" does not exist!")
3345 return {'CANCELLED'}
3347 without_ext = '.'.join(filename.split('.')[:-1])
3349 # if last digit isn't a number, it's not a sequence
3350 if not without_ext[-1].isdigit():
3351 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3352 return {'CANCELLED'}
3355 extension = filename.split('.')[-1]
3356 reverse = without_ext[::-1] # reverse string
3358 count_numbers = 0
3359 for char in reverse:
3360 if char.isdigit():
3361 count_numbers += 1
3362 else:
3363 break
3365 without_num = without_ext[:count_numbers*-1]
3367 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3369 num_frames = len(files)
3371 nodes_list = [node for node in nodes]
3372 if nodes_list:
3373 nodes_list.sort(key=lambda k: k.location.x)
3374 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3375 yloc = 0
3376 for node in nodes:
3377 node.select = False
3378 yloc += node_mid_pt(node, 'y')
3379 yloc = yloc/len(nodes)
3380 else:
3381 xloc = 0
3382 yloc = 0
3384 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3386 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3387 node = nodes.active
3388 node.label = name_with_hashes
3390 img = bpy.data.images.load(directory+(without_ext+'.'+extension))
3391 img.source = 'SEQUENCE'
3392 img.name = name_with_hashes
3393 node.image = img
3394 image_user = node.image_user if tree.type == 'SHADER' else node
3395 image_user.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
3396 image_user.frame_duration = num_frames
3398 return {'FINISHED'}
3401 class NWAddMultipleImages(Operator, ImportHelper):
3402 """Add multiple images at once"""
3403 bl_idname = 'node.nw_add_multiple_images'
3404 bl_label = 'Open Selected Images'
3405 bl_options = {'REGISTER', 'UNDO'}
3406 directory = StringProperty(subtype="DIR_PATH")
3407 files = CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
3409 def execute(self, context):
3410 nodes, links = get_nodes_links(context)
3412 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3414 if context.space_data.node_tree.type == 'SHADER':
3415 node_type = "ShaderNodeTexImage"
3416 elif context.space_data.node_tree.type == 'COMPOSITING':
3417 node_type = "CompositorNodeImage"
3418 else:
3419 self.report({'ERROR'}, "Unsupported Node Tree type!")
3420 return {'CANCELLED'}
3422 new_nodes = []
3423 for f in self.files:
3424 fname = f.name
3426 node = nodes.new(node_type)
3427 new_nodes.append(node)
3428 node.label = fname
3429 node.hide = True
3430 node.width_hidden = 100
3431 node.location.x = xloc
3432 node.location.y = yloc
3433 yloc -= 40
3435 img = bpy.data.images.load(self.directory+fname)
3436 node.image = img
3438 # shift new nodes up to center of tree
3439 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3440 for node in nodes:
3441 if node in new_nodes:
3442 node.select = True
3443 node.location.y += (list_size/2)
3444 else:
3445 node.select = False
3446 return {'FINISHED'}
3449 class NWViewerFocus(bpy.types.Operator):
3450 """Set the viewer tile center to the mouse position"""
3451 bl_idname = "node.nw_viewer_focus"
3452 bl_label = "Viewer Focus"
3454 x: bpy.props.IntProperty()
3455 y: bpy.props.IntProperty()
3457 @classmethod
3458 def poll(cls, context):
3459 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3461 def execute(self, context):
3462 return {'FINISHED'}
3464 def invoke(self, context, event):
3465 render = context.scene.render
3466 space = context.space_data
3467 percent = render.resolution_percentage*0.01
3469 nodes, links = get_nodes_links(context)
3470 viewers = [n for n in nodes if n.type == 'VIEWER']
3472 if viewers:
3473 mlocx = event.mouse_region_x
3474 mlocy = event.mouse_region_y
3475 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
3477 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3478 region_x = context.region.width
3479 region_y = context.region.height
3481 region_center_x = context.region.width / 2
3482 region_center_y = context.region.height / 2
3484 bd_x = render.resolution_x * percent * space.backdrop_zoom
3485 bd_y = render.resolution_y * percent * space.backdrop_zoom
3487 backdrop_center_x = (bd_x / 2) - space.backdrop_x
3488 backdrop_center_y = (bd_y / 2) - space.backdrop_y
3490 margin_x = region_center_x - backdrop_center_x
3491 margin_y = region_center_y - backdrop_center_y
3493 abs_mouse_x = (mlocx - margin_x) / bd_x
3494 abs_mouse_y = (mlocy - margin_y) / bd_y
3496 for node in viewers:
3497 node.center_x = abs_mouse_x
3498 node.center_y = abs_mouse_y
3499 else:
3500 return {'PASS_THROUGH'}
3502 return self.execute(context)
3505 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3506 """Save the current viewer node to an image file"""
3507 bl_idname = "node.nw_save_viewer"
3508 bl_label = "Save This Image"
3509 filepath: StringProperty(subtype="FILE_PATH")
3510 filename_ext: EnumProperty(
3511 name="Format",
3512 description="Choose the file format to save to",
3513 items=(('.bmp', "PNG", ""),
3514 ('.rgb', 'IRIS', ""),
3515 ('.png', 'PNG', ""),
3516 ('.jpg', 'JPEG', ""),
3517 ('.jp2', 'JPEG2000', ""),
3518 ('.tga', 'TARGA', ""),
3519 ('.cin', 'CINEON', ""),
3520 ('.dpx', 'DPX', ""),
3521 ('.exr', 'OPEN_EXR', ""),
3522 ('.hdr', 'HDR', ""),
3523 ('.tif', 'TIFF', "")),
3524 default='.png',
3527 @classmethod
3528 def poll(cls, context):
3529 valid = False
3530 if nw_check(context):
3531 if context.space_data.tree_type == 'CompositorNodeTree':
3532 if "Viewer Node" in [i.name for i in bpy.data.images]:
3533 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3534 valid = True
3535 return valid
3537 def execute(self, context):
3538 fp = self.filepath
3539 if fp:
3540 formats = {
3541 '.bmp': 'BMP',
3542 '.rgb': 'IRIS',
3543 '.png': 'PNG',
3544 '.jpg': 'JPEG',
3545 '.jpeg': 'JPEG',
3546 '.jp2': 'JPEG2000',
3547 '.tga': 'TARGA',
3548 '.cin': 'CINEON',
3549 '.dpx': 'DPX',
3550 '.exr': 'OPEN_EXR',
3551 '.hdr': 'HDR',
3552 '.tiff': 'TIFF',
3553 '.tif': 'TIFF'}
3554 basename, ext = path.splitext(fp)
3555 old_render_format = context.scene.render.image_settings.file_format
3556 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3557 context.area.type = "IMAGE_EDITOR"
3558 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3559 context.area.spaces[0].image.save_render(fp)
3560 context.area.type = "NODE_EDITOR"
3561 context.scene.render.image_settings.file_format = old_render_format
3562 return {'FINISHED'}
3565 class NWResetNodes(bpy.types.Operator):
3566 """Reset Nodes in Selection"""
3567 bl_idname = "node.nw_reset_nodes"
3568 bl_label = "Reset Nodes"
3569 bl_options = {'REGISTER', 'UNDO'}
3571 @classmethod
3572 def poll(cls, context):
3573 space = context.space_data
3574 return space.type == 'NODE_EDITOR'
3576 def execute(self, context):
3577 node_active = context.active_node
3578 node_selected = context.selected_nodes
3579 node_ignore = ["FRAME","REROUTE", "GROUP"]
3581 # Check if one node is selected at least
3582 if not (len(node_selected) > 0):
3583 self.report({'ERROR'}, "1 node must be selected at least")
3584 return {'CANCELLED'}
3586 active_node_name = node_active.name if node_active.select else None
3587 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3589 # Create output lists
3590 selected_node_names = [n.name for n in node_selected]
3591 success_names = []
3593 # Reset all valid children in a frame
3594 node_active_is_frame = False
3595 if len(node_selected) == 1 and node_active.type == "FRAME":
3596 node_tree = node_active.id_data
3597 children = [n for n in node_tree.nodes if n.parent == node_active]
3598 if children:
3599 valid_nodes = [n for n in children if n.type not in node_ignore]
3600 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3601 node_active_is_frame = True
3603 # Check if valid nodes in selection
3604 if not (len(valid_nodes) > 0):
3605 # Check for frames only
3606 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3607 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3608 self.report({'ERROR'}, "Please select only 1 frame to reset")
3609 else:
3610 self.report({'ERROR'}, "No valid node(s) in selection")
3611 return {'CANCELLED'}
3613 # Report nodes that are not valid
3614 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3615 valid_node_names = [n.name for n in valid_nodes]
3616 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3617 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3619 # Deselect all nodes
3620 for i in node_selected:
3621 i.select = False
3623 # Run through all valid nodes
3624 for node in valid_nodes:
3626 parent = node.parent if node.parent else None
3627 node_loc = [node.location.x, node.location.y]
3629 node_tree = node.id_data
3630 props_to_copy = 'bl_idname name location height width'.split(' ')
3632 reconnections = []
3633 mappings = chain.from_iterable([node.inputs, node.outputs])
3634 for i in (i for i in mappings if i.is_linked):
3635 for L in i.links:
3636 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3638 props = {j: getattr(node, j) for j in props_to_copy}
3640 new_node = node_tree.nodes.new(props['bl_idname'])
3641 props_to_copy.pop(0)
3643 for prop in props_to_copy:
3644 setattr(new_node, prop, props[prop])
3646 nodes = node_tree.nodes
3647 nodes.remove(node)
3648 new_node.name = props['name']
3650 if parent:
3651 new_node.parent = parent
3652 new_node.location = node_loc
3654 for str_from, str_to in reconnections:
3655 node_tree.links.new(eval(str_from), eval(str_to))
3657 new_node.select = False
3658 success_names.append(new_node.name)
3660 # Reselect all nodes
3661 if selected_node_names and node_active_is_frame is False:
3662 for i in selected_node_names:
3663 node_tree.nodes[i].select = True
3665 if active_node_name is not None:
3666 node_tree.nodes[active_node_name].select = True
3667 node_tree.nodes.active = node_tree.nodes[active_node_name]
3669 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3670 return {'FINISHED'}
3674 # P A N E L
3677 def drawlayout(context, layout, mode='non-panel'):
3678 tree_type = context.space_data.tree_type
3680 col = layout.column(align=True)
3681 col.menu(NWMergeNodesMenu.bl_idname)
3682 col.separator()
3684 col = layout.column(align=True)
3685 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3686 col.separator()
3688 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3689 col = layout.column(align=True)
3690 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3691 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3692 col.separator()
3694 col = layout.column(align=True)
3695 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3696 col.operator(NWSwapLinks.bl_idname)
3697 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3698 col.separator()
3700 col = layout.column(align=True)
3701 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3702 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3703 col.separator()
3705 col = layout.column(align=True)
3706 if mode == 'panel':
3707 row = col.row(align=True)
3708 row.operator(NWClearLabel.bl_idname).option = True
3709 row.operator(NWModifyLabels.bl_idname)
3710 else:
3711 col.operator(NWClearLabel.bl_idname).option = True
3712 col.operator(NWModifyLabels.bl_idname)
3713 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3714 col.separator()
3715 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3716 col.separator()
3718 col = layout.column(align=True)
3719 if tree_type == 'CompositorNodeTree':
3720 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
3721 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
3722 col.separator()
3724 col = layout.column(align=True)
3725 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3726 col.separator()
3728 col = layout.column(align=True)
3729 col.operator(NWAlignNodes.bl_idname, icon='ALIGN')
3730 col.separator()
3732 col = layout.column(align=True)
3733 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3734 col.separator()
3737 class NodeWranglerPanel(Panel, NWBase):
3738 bl_idname = "NODE_PT_nw_node_wrangler"
3739 bl_space_type = 'NODE_EDITOR'
3740 bl_label = "Node Wrangler"
3741 bl_region_type = "TOOLS"
3742 bl_category = "Node Wrangler"
3744 prepend: StringProperty(
3745 name='prepend',
3747 append: StringProperty()
3748 remove: StringProperty()
3750 def draw(self, context):
3751 self.layout.label(text="(Quick access: Ctrl+Space)")
3752 drawlayout(context, self.layout, mode='panel')
3756 # M E N U S
3758 class NodeWranglerMenu(Menu, NWBase):
3759 bl_idname = "NODE_MT_nw_node_wrangler_menu"
3760 bl_label = "Node Wrangler"
3762 def draw(self, context):
3763 drawlayout(context, self.layout)
3766 class NWMergeNodesMenu(Menu, NWBase):
3767 bl_idname = "NODE_MT_nw_merge_nodes_menu"
3768 bl_label = "Merge Selected Nodes"
3770 def draw(self, context):
3771 type = context.space_data.tree_type
3772 layout = self.layout
3773 if type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3774 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
3775 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
3776 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3777 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
3778 props.mode = 'MIX'
3779 props.merge_type = 'ZCOMBINE'
3780 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3781 props.mode = 'MIX'
3782 props.merge_type = 'ALPHAOVER'
3785 class NWMergeShadersMenu(Menu, NWBase):
3786 bl_idname = "NODE_MT_nw_merge_shaders_menu"
3787 bl_label = "Merge Selected Nodes using Shaders"
3789 def draw(self, context):
3790 layout = self.layout
3791 for type in ('MIX', 'ADD'):
3792 props = layout.operator(NWMergeNodes.bl_idname, text=type)
3793 props.mode = type
3794 props.merge_type = 'SHADER'
3797 class NWMergeMixMenu(Menu, NWBase):
3798 bl_idname = "NODE_MT_nw_merge_mix_menu"
3799 bl_label = "Merge Selected Nodes using Mix"
3801 def draw(self, context):
3802 layout = self.layout
3803 for type, name, description in blend_types:
3804 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3805 props.mode = type
3806 props.merge_type = 'MIX'
3809 class NWConnectionListOutputs(Menu, NWBase):
3810 bl_idname = "NODE_MT_nw_connection_list_out"
3811 bl_label = "From:"
3813 def draw(self, context):
3814 layout = self.layout
3815 nodes, links = get_nodes_links(context)
3817 n1 = nodes[context.scene.NWLazySource]
3819 if n1.type == "R_LAYERS":
3820 index=0
3821 for o in n1.outputs:
3822 if o.enabled: # Check which passes the render layer has enabled
3823 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3824 index+=1
3825 else:
3826 index=0
3827 for o in n1.outputs:
3828 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3829 index+=1
3832 class NWConnectionListInputs(Menu, NWBase):
3833 bl_idname = "NODE_MT_nw_connection_list_in"
3834 bl_label = "To:"
3836 def draw(self, context):
3837 layout = self.layout
3838 nodes, links = get_nodes_links(context)
3840 n2 = nodes[context.scene.NWLazyTarget]
3842 index = 0
3843 for i in n2.inputs:
3844 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
3845 op.from_socket = context.scene.NWSourceSocket
3846 op.to_socket = index
3847 index+=1
3850 class NWMergeMathMenu(Menu, NWBase):
3851 bl_idname = "NODE_MT_nw_merge_math_menu"
3852 bl_label = "Merge Selected Nodes using Math"
3854 def draw(self, context):
3855 layout = self.layout
3856 for type, name, description in operations:
3857 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3858 props.mode = type
3859 props.merge_type = 'MATH'
3862 class NWBatchChangeNodesMenu(Menu, NWBase):
3863 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3864 bl_label = "Batch Change Selected Nodes"
3866 def draw(self, context):
3867 layout = self.layout
3868 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3869 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3872 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3873 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3874 bl_label = "Batch Change Blend Type"
3876 def draw(self, context):
3877 layout = self.layout
3878 for type, name, description in blend_types:
3879 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3880 props.blend_type = type
3881 props.operation = 'CURRENT'
3884 class NWBatchChangeOperationMenu(Menu, NWBase):
3885 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3886 bl_label = "Batch Change Math Operation"
3888 def draw(self, context):
3889 layout = self.layout
3890 for type, name, description in operations:
3891 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3892 props.blend_type = 'CURRENT'
3893 props.operation = type
3896 class NWCopyToSelectedMenu(Menu, NWBase):
3897 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
3898 bl_label = "Copy to Selected"
3900 def draw(self, context):
3901 layout = self.layout
3902 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
3903 layout.menu(NWCopyLabelMenu.bl_idname)
3906 class NWCopyLabelMenu(Menu, NWBase):
3907 bl_idname = "NODE_MT_nw_copy_label_menu"
3908 bl_label = "Copy Label"
3910 def draw(self, context):
3911 layout = self.layout
3912 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
3913 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
3914 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
3917 class NWAddReroutesMenu(Menu, NWBase):
3918 bl_idname = "NODE_MT_nw_add_reroutes_menu"
3919 bl_label = "Add Reroutes"
3920 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
3922 def draw(self, context):
3923 layout = self.layout
3924 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
3925 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
3926 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
3929 class NWLinkActiveToSelectedMenu(Menu, NWBase):
3930 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
3931 bl_label = "Link Active to Selected"
3933 def draw(self, context):
3934 layout = self.layout
3935 layout.menu(NWLinkStandardMenu.bl_idname)
3936 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
3937 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
3940 class NWLinkStandardMenu(Menu, NWBase):
3941 bl_idname = "NODE_MT_nw_link_standard_menu"
3942 bl_label = "To All Selected"
3944 def draw(self, context):
3945 layout = self.layout
3946 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3947 props.replace = False
3948 props.use_node_name = False
3949 props.use_outputs_names = False
3950 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3951 props.replace = True
3952 props.use_node_name = False
3953 props.use_outputs_names = False
3956 class NWLinkUseNodeNameMenu(Menu, NWBase):
3957 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
3958 bl_label = "Use Node Name/Label"
3960 def draw(self, context):
3961 layout = self.layout
3962 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3963 props.replace = False
3964 props.use_node_name = True
3965 props.use_outputs_names = False
3966 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3967 props.replace = True
3968 props.use_node_name = True
3969 props.use_outputs_names = False
3972 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
3973 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
3974 bl_label = "Use Outputs Names"
3976 def draw(self, context):
3977 layout = self.layout
3978 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3979 props.replace = False
3980 props.use_node_name = False
3981 props.use_outputs_names = True
3982 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3983 props.replace = True
3984 props.use_node_name = False
3985 props.use_outputs_names = True
3988 class NWVertColMenu(bpy.types.Menu):
3989 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
3990 bl_label = "Vertex Colors"
3992 @classmethod
3993 def poll(cls, context):
3994 valid = False
3995 if nw_check(context):
3996 snode = context.space_data
3997 valid = snode.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context)
3998 return valid
4000 def draw(self, context):
4001 l = self.layout
4002 nodes, links = get_nodes_links(context)
4003 mat = context.object.active_material
4005 objs = []
4006 for obj in bpy.data.objects:
4007 for slot in obj.material_slots:
4008 if slot.material == mat:
4009 objs.append(obj)
4010 vcols = []
4011 for obj in objs:
4012 if obj.data.vertex_colors:
4013 for vcol in obj.data.vertex_colors:
4014 vcols.append(vcol.name)
4015 vcols = list(set(vcols)) # get a unique list
4017 if vcols:
4018 for vcol in vcols:
4019 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
4020 else:
4021 l.label("No Vertex Color layers on objects with this material")
4024 class NWSwitchNodeTypeMenu(Menu, NWBase):
4025 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4026 bl_label = "Switch Type to..."
4028 def draw(self, context):
4029 layout = self.layout
4030 tree = context.space_data.node_tree
4031 if tree.type == 'SHADER':
4032 if is_cycles_or_eevee(context):
4033 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
4034 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
4035 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
4036 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
4037 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
4038 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
4039 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
4040 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
4041 else:
4042 layout.menu(NWSwitchMatInputSubmenu.bl_idname)
4043 layout.menu(NWSwitchMatOutputSubmenu.bl_idname)
4044 layout.menu(NWSwitchMatColorSubmenu.bl_idname)
4045 layout.menu(NWSwitchMatVectorSubmenu.bl_idname)
4046 layout.menu(NWSwitchMatConverterSubmenu.bl_idname)
4047 layout.menu(NWSwitchMatLayoutSubmenu.bl_idname)
4048 if tree.type == 'COMPOSITING':
4049 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
4050 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
4051 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
4052 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
4053 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
4054 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
4055 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
4056 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
4057 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
4058 if tree.type == 'TEXTURE':
4059 layout.menu(NWSwitchTexInputSubmenu.bl_idname)
4060 layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
4061 layout.menu(NWSwitchTexColorSubmenu.bl_idname)
4062 layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
4063 layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
4064 layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
4065 layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
4066 layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
4069 class NWSwitchShadersInputSubmenu(Menu, NWBase):
4070 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
4071 bl_label = "Input"
4073 def draw(self, context):
4074 layout = self.layout
4075 for ident, node_type, rna_name in sorted(shaders_input_nodes_props, key=lambda k: k[2]):
4076 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4077 props.to_type = ident
4080 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
4081 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
4082 bl_label = "Output"
4084 def draw(self, context):
4085 layout = self.layout
4086 for ident, node_type, rna_name in sorted(shaders_output_nodes_props, key=lambda k: k[2]):
4087 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4088 props.to_type = ident
4091 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
4092 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
4093 bl_label = "Shader"
4095 def draw(self, context):
4096 layout = self.layout
4097 for ident, node_type, rna_name in sorted(shaders_shader_nodes_props, key=lambda k: k[2]):
4098 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4099 props.to_type = ident
4102 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
4103 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
4104 bl_label = "Texture"
4106 def draw(self, context):
4107 layout = self.layout
4108 for ident, node_type, rna_name in sorted(shaders_texture_nodes_props, key=lambda k: k[2]):
4109 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4110 props.to_type = ident
4113 class NWSwitchShadersColorSubmenu(Menu, NWBase):
4114 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
4115 bl_label = "Color"
4117 def draw(self, context):
4118 layout = self.layout
4119 for ident, node_type, rna_name in sorted(shaders_color_nodes_props, key=lambda k: k[2]):
4120 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4121 props.to_type = ident
4124 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
4125 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
4126 bl_label = "Vector"
4128 def draw(self, context):
4129 layout = self.layout
4130 for ident, node_type, rna_name in sorted(shaders_vector_nodes_props, key=lambda k: k[2]):
4131 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4132 props.to_type = ident
4135 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
4136 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
4137 bl_label = "Converter"
4139 def draw(self, context):
4140 layout = self.layout
4141 for ident, node_type, rna_name in sorted(shaders_converter_nodes_props, key=lambda k: k[2]):
4142 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4143 props.to_type = ident
4146 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
4147 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
4148 bl_label = "Layout"
4150 def draw(self, context):
4151 layout = self.layout
4152 for ident, node_type, rna_name in sorted(shaders_layout_nodes_props, key=lambda k: k[2]):
4153 if node_type != 'FRAME':
4154 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4155 props.to_type = ident
4158 class NWSwitchCompoInputSubmenu(Menu, NWBase):
4159 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
4160 bl_label = "Input"
4162 def draw(self, context):
4163 layout = self.layout
4164 for ident, node_type, rna_name in sorted(compo_input_nodes_props, key=lambda k: k[2]):
4165 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4166 props.to_type = ident
4169 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
4170 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
4171 bl_label = "Output"
4173 def draw(self, context):
4174 layout = self.layout
4175 for ident, node_type, rna_name in sorted(compo_output_nodes_props, key=lambda k: k[2]):
4176 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4177 props.to_type = ident
4180 class NWSwitchCompoColorSubmenu(Menu, NWBase):
4181 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
4182 bl_label = "Color"
4184 def draw(self, context):
4185 layout = self.layout
4186 for ident, node_type, rna_name in sorted(compo_color_nodes_props, key=lambda k: k[2]):
4187 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4188 props.to_type = ident
4191 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
4192 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
4193 bl_label = "Converter"
4195 def draw(self, context):
4196 layout = self.layout
4197 for ident, node_type, rna_name in sorted(compo_converter_nodes_props, key=lambda k: k[2]):
4198 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4199 props.to_type = ident
4202 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
4203 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
4204 bl_label = "Filter"
4206 def draw(self, context):
4207 layout = self.layout
4208 for ident, node_type, rna_name in sorted(compo_filter_nodes_props, key=lambda k: k[2]):
4209 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4210 props.to_type = ident
4213 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
4214 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
4215 bl_label = "Vector"
4217 def draw(self, context):
4218 layout = self.layout
4219 for ident, node_type, rna_name in sorted(compo_vector_nodes_props, key=lambda k: k[2]):
4220 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4221 props.to_type = ident
4224 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
4225 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
4226 bl_label = "Matte"
4228 def draw(self, context):
4229 layout = self.layout
4230 for ident, node_type, rna_name in sorted(compo_matte_nodes_props, key=lambda k: k[2]):
4231 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4232 props.to_type = ident
4235 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
4236 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
4237 bl_label = "Distort"
4239 def draw(self, context):
4240 layout = self.layout
4241 for ident, node_type, rna_name in sorted(compo_distort_nodes_props, key=lambda k: k[2]):
4242 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4243 props.to_type = ident
4246 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
4247 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
4248 bl_label = "Layout"
4250 def draw(self, context):
4251 layout = self.layout
4252 for ident, node_type, rna_name in sorted(compo_layout_nodes_props, key=lambda k: k[2]):
4253 if node_type != 'FRAME':
4254 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4255 props.to_type = ident
4258 class NWSwitchMatInputSubmenu(Menu, NWBase):
4259 bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
4260 bl_label = "Input"
4262 def draw(self, context):
4263 layout = self.layout
4264 for ident, node_type, rna_name in sorted(blender_mat_input_nodes_props, key=lambda k: k[2]):
4265 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4266 props.to_type = ident
4269 class NWSwitchMatOutputSubmenu(Menu, NWBase):
4270 bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
4271 bl_label = "Output"
4273 def draw(self, context):
4274 layout = self.layout
4275 for ident, node_type, rna_name in sorted(blender_mat_output_nodes_props, key=lambda k: k[2]):
4276 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4277 props.to_type = ident
4280 class NWSwitchMatColorSubmenu(Menu, NWBase):
4281 bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
4282 bl_label = "Color"
4284 def draw(self, context):
4285 layout = self.layout
4286 for ident, node_type, rna_name in sorted(blender_mat_color_nodes_props, key=lambda k: k[2]):
4287 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4288 props.to_type = ident
4291 class NWSwitchMatVectorSubmenu(Menu, NWBase):
4292 bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
4293 bl_label = "Vector"
4295 def draw(self, context):
4296 layout = self.layout
4297 for ident, node_type, rna_name in sorted(blender_mat_vector_nodes_props, key=lambda k: k[2]):
4298 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4299 props.to_type = ident
4302 class NWSwitchMatConverterSubmenu(Menu, NWBase):
4303 bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
4304 bl_label = "Converter"
4306 def draw(self, context):
4307 layout = self.layout
4308 for ident, node_type, rna_name in sorted(blender_mat_converter_nodes_props, key=lambda k: k[2]):
4309 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4310 props.to_type = ident
4313 class NWSwitchMatLayoutSubmenu(Menu, NWBase):
4314 bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
4315 bl_label = "Layout"
4317 def draw(self, context):
4318 layout = self.layout
4319 for ident, node_type, rna_name in sorted(blender_mat_layout_nodes_props, key=lambda k: k[2]):
4320 if node_type != 'FRAME':
4321 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4322 props.to_type = ident
4325 class NWSwitchTexInputSubmenu(Menu, NWBase):
4326 bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
4327 bl_label = "Input"
4329 def draw(self, context):
4330 layout = self.layout
4331 for ident, node_type, rna_name in sorted(texture_input_nodes_props, key=lambda k: k[2]):
4332 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4333 props.to_type = ident
4336 class NWSwitchTexOutputSubmenu(Menu, NWBase):
4337 bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
4338 bl_label = "Output"
4340 def draw(self, context):
4341 layout = self.layout
4342 for ident, node_type, rna_name in sorted(texture_output_nodes_props, key=lambda k: k[2]):
4343 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4344 props.to_type = ident
4347 class NWSwitchTexColorSubmenu(Menu, NWBase):
4348 bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
4349 bl_label = "Color"
4351 def draw(self, context):
4352 layout = self.layout
4353 for ident, node_type, rna_name in sorted(texture_color_nodes_props, key=lambda k: k[2]):
4354 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4355 props.to_type = ident
4358 class NWSwitchTexPatternSubmenu(Menu, NWBase):
4359 bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
4360 bl_label = "Pattern"
4362 def draw(self, context):
4363 layout = self.layout
4364 for ident, node_type, rna_name in sorted(texture_pattern_nodes_props, key=lambda k: k[2]):
4365 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4366 props.to_type = ident
4369 class NWSwitchTexTexturesSubmenu(Menu, NWBase):
4370 bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
4371 bl_label = "Textures"
4373 def draw(self, context):
4374 layout = self.layout
4375 for ident, node_type, rna_name in sorted(texture_textures_nodes_props, key=lambda k: k[2]):
4376 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4377 props.to_type = ident
4380 class NWSwitchTexConverterSubmenu(Menu, NWBase):
4381 bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
4382 bl_label = "Converter"
4384 def draw(self, context):
4385 layout = self.layout
4386 for ident, node_type, rna_name in sorted(texture_converter_nodes_props, key=lambda k: k[2]):
4387 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4388 props.to_type = ident
4391 class NWSwitchTexDistortSubmenu(Menu, NWBase):
4392 bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
4393 bl_label = "Distort"
4395 def draw(self, context):
4396 layout = self.layout
4397 for ident, node_type, rna_name in sorted(texture_distort_nodes_props, key=lambda k: k[2]):
4398 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4399 props.to_type = ident
4402 class NWSwitchTexLayoutSubmenu(Menu, NWBase):
4403 bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
4404 bl_label = "Layout"
4406 def draw(self, context):
4407 layout = self.layout
4408 for ident, node_type, rna_name in sorted(texture_layout_nodes_props, key=lambda k: k[2]):
4409 if node_type != 'FRAME':
4410 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4411 props.to_type = ident
4415 # APPENDAGES TO EXISTING UI
4419 def select_parent_children_buttons(self, context):
4420 layout = self.layout
4421 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4422 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4425 def attr_nodes_menu_func(self, context):
4426 col = self.layout.column(align=True)
4427 col.menu("NODE_MT_nw_node_vertex_color_menu")
4428 col.separator()
4431 def multipleimages_menu_func(self, context):
4432 col = self.layout.column(align=True)
4433 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4434 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4435 col.separator()
4438 def bgreset_menu_func(self, context):
4439 self.layout.operator(NWResetBG.bl_idname)
4442 def save_viewer_menu_func(self, context):
4443 if nw_check(context):
4444 if context.space_data.tree_type == 'CompositorNodeTree':
4445 if context.scene.node_tree.nodes.active:
4446 if context.scene.node_tree.nodes.active.type == "VIEWER":
4447 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4450 def reset_nodes_button(self, context):
4451 node_active = context.active_node
4452 node_selected = context.selected_nodes
4453 node_ignore = ["FRAME","REROUTE", "GROUP"]
4455 # Check if active node is in the selection and respective type
4456 if (len(node_selected) == 1) and node_active.select and node_active.type not in node_ignore:
4457 row = self.layout.row()
4458 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4459 self.layout.separator()
4461 elif (len(node_selected) == 1) and node_active.select and node_active.type == "FRAME":
4462 row = self.layout.row()
4463 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4464 self.layout.separator()
4468 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4471 addon_keymaps = []
4472 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4473 # props entry: (property name, property value)
4474 kmi_defs = (
4475 # MERGE NODES
4476 # NWMergeNodes with Ctrl (AUTO).
4477 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4478 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4479 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4480 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4481 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4482 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4483 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4484 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4485 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4486 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4487 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4488 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4489 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4490 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4491 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4492 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4493 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4494 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4495 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4496 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4497 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4498 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4499 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4500 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4501 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4502 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4503 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4504 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4505 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4506 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4507 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4508 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4509 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4510 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4511 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4512 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4513 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4514 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4515 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4516 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4517 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4518 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4519 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4520 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4521 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4522 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4523 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4524 # NWMergeNodes with Ctrl Shift (MATH)
4525 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4526 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4527 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4528 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4529 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4530 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4531 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4532 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4533 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4534 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4535 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4536 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4537 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4538 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4539 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4540 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4541 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4542 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4543 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4544 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4545 # BATCH CHANGE NODES
4546 # NWBatchChangeNodes with Alt
4547 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4548 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4549 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4550 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4551 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4552 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4553 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4554 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4555 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4556 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4557 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4558 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4559 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4560 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4561 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4562 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4563 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4564 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4565 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4566 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4567 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4568 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4569 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4570 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4571 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4572 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4573 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4574 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4575 # LINK ACTIVE TO SELECTED
4576 # Don't use names, don't replace links (K)
4577 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4578 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4579 # Don't use names, replace links (Shift K)
4580 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4581 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4582 # Use node name, don't replace links (')
4583 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4584 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4585 # Use node name, replace links (Shift ')
4586 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4587 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4588 # Don't use names, don't replace links (;)
4589 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4590 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4591 # Don't use names, replace links (')
4592 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4593 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4594 # CHANGE MIX FACTOR
4595 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4596 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4597 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4598 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4599 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4600 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4601 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4602 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4603 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4604 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4605 # CLEAR LABEL (Alt L)
4606 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4607 # MODIFY LABEL (Alt Shift L)
4608 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4609 # Copy Label from active to selected
4610 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4611 # DETACH OUTPUTS (Alt Shift D)
4612 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4613 # LINK TO OUTPUT NODE (O)
4614 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4615 # SELECT PARENT/CHILDREN
4616 # Select Children
4617 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4618 # Select Parent
4619 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4620 # Add Texture Setup
4621 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4622 # Add Principled BSDF Texture Setup
4623 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4624 # Reset backdrop
4625 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4626 # Delete unused
4627 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4628 # Frame Seleted
4629 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4630 # Swap Outputs
4631 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4632 # Emission Viewer
4633 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4634 # Reload Images
4635 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4636 # Lazy Mix
4637 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, None, "Lazy Mix"),
4638 # Lazy Connect
4639 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, False, False, None, "Lazy Connect"),
4640 # Lazy Connect with Menu
4641 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4642 # Viewer Tile Center
4643 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4644 # Align Nodes
4645 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4646 # Reset Nodes (Back Space)
4647 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4648 # MENUS
4649 ('wm.call_menu', 'SPACE', 'PRESS', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"),
4650 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4651 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4652 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4653 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4654 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4658 classes = (
4659 NWPrincipledPreferences,
4660 NWNodeWrangler,
4661 NWLazyMix,
4662 NWLazyConnect,
4663 NWDeleteUnused,
4664 NWSwapLinks,
4665 NWResetBG,
4666 NWAddAttrNode,
4667 NWEmissionViewer,
4668 NWFrameSelected,
4669 NWReloadImages,
4670 NWSwitchNodeType,
4671 NWMergeNodes,
4672 NWBatchChangeNodes,
4673 NWChangeMixFactor,
4674 NWCopySettings,
4675 NWCopyLabel,
4676 NWClearLabel,
4677 NWModifyLabels,
4678 NWAddTextureSetup,
4679 NWAddPrincipledSetup,
4680 NWAddReroutes,
4681 NWLinkActiveToSelected,
4682 NWAlignNodes,
4683 NWSelectParentChildren,
4684 NWDetachOutputs,
4685 NWLinkToOutputNode,
4686 NWMakeLink,
4687 NWCallInputsMenu,
4688 NWAddSequence,
4689 NWAddMultipleImages,
4690 NWViewerFocus,
4691 NWSaveViewer,
4692 NWResetNodes,
4693 NodeWranglerPanel,
4694 NodeWranglerMenu,
4695 NWMergeNodesMenu,
4696 NWMergeShadersMenu,
4697 NWMergeMixMenu,
4698 NWConnectionListOutputs,
4699 NWConnectionListInputs,
4700 NWMergeMathMenu,
4701 NWBatchChangeNodesMenu,
4702 NWBatchChangeBlendTypeMenu,
4703 NWBatchChangeOperationMenu,
4704 NWCopyToSelectedMenu,
4705 NWCopyLabelMenu,
4706 NWAddReroutesMenu,
4707 NWLinkActiveToSelectedMenu,
4708 NWLinkStandardMenu,
4709 NWLinkUseNodeNameMenu,
4710 NWLinkUseOutputsNamesMenu,
4711 NWVertColMenu,
4712 NWSwitchNodeTypeMenu,
4713 NWSwitchShadersInputSubmenu,
4714 NWSwitchShadersOutputSubmenu,
4715 NWSwitchShadersShaderSubmenu,
4716 NWSwitchShadersTextureSubmenu,
4717 NWSwitchShadersColorSubmenu,
4718 NWSwitchShadersVectorSubmenu,
4719 NWSwitchShadersConverterSubmenu,
4720 NWSwitchShadersLayoutSubmenu,
4721 NWSwitchCompoInputSubmenu,
4722 NWSwitchCompoOutputSubmenu,
4723 NWSwitchCompoColorSubmenu,
4724 NWSwitchCompoConverterSubmenu,
4725 NWSwitchCompoFilterSubmenu,
4726 NWSwitchCompoVectorSubmenu,
4727 NWSwitchCompoMatteSubmenu,
4728 NWSwitchCompoDistortSubmenu,
4729 NWSwitchCompoLayoutSubmenu,
4730 NWSwitchMatInputSubmenu,
4731 NWSwitchMatOutputSubmenu,
4732 NWSwitchMatColorSubmenu,
4733 NWSwitchMatVectorSubmenu,
4734 NWSwitchMatConverterSubmenu,
4735 NWSwitchMatLayoutSubmenu,
4736 NWSwitchTexInputSubmenu,
4737 NWSwitchTexOutputSubmenu,
4738 NWSwitchTexColorSubmenu,
4739 NWSwitchTexPatternSubmenu,
4740 NWSwitchTexTexturesSubmenu,
4741 NWSwitchTexConverterSubmenu,
4742 NWSwitchTexDistortSubmenu,
4743 NWSwitchTexLayoutSubmenu,
4746 def register():
4747 from bpy.utils import register_class
4749 # props
4750 bpy.types.Scene.NWBusyDrawing = StringProperty(
4751 name="Busy Drawing!",
4752 default="",
4753 description="An internal property used to store only the first mouse position")
4754 bpy.types.Scene.NWLazySource = StringProperty(
4755 name="Lazy Source!",
4756 default="x",
4757 description="An internal property used to store the first node in a Lazy Connect operation")
4758 bpy.types.Scene.NWLazyTarget = StringProperty(
4759 name="Lazy Target!",
4760 default="x",
4761 description="An internal property used to store the last node in a Lazy Connect operation")
4762 bpy.types.Scene.NWSourceSocket = IntProperty(
4763 name="Source Socket!",
4764 default=0,
4765 description="An internal property used to store the source socket in a Lazy Connect operation")
4767 for cls in classes:
4768 register_class(cls)
4770 # keymaps
4771 addon_keymaps.clear()
4772 kc = bpy.context.window_manager.keyconfigs.addon
4773 if kc:
4774 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
4775 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
4776 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
4777 if props:
4778 for prop, value in props:
4779 setattr(kmi.properties, prop, value)
4780 addon_keymaps.append((km, kmi))
4782 # menu items
4783 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
4784 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4785 bpy.types.NODE_PT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4786 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
4787 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
4788 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4789 bpy.types.NODE_PT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4790 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4791 bpy.types.NODE_PT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4792 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
4793 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
4796 def unregister():
4797 from bpy.utils import unregister_class
4799 # props
4800 del bpy.types.Scene.NWBusyDrawing
4801 del bpy.types.Scene.NWLazySource
4802 del bpy.types.Scene.NWLazyTarget
4803 del bpy.types.Scene.NWSourceSocket
4805 # keymaps
4806 for km, kmi in addon_keymaps:
4807 km.keymap_items.remove(kmi)
4808 addon_keymaps.clear()
4810 # menuitems
4811 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
4812 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4813 bpy.types.NODE_PT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4814 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
4815 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
4816 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4817 bpy.types.NODE_PT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4818 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
4819 bpy.types.NODE_PT_category_CMP_INPUT.remove(multipleimages_menu_func)
4820 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
4821 bpy.types.NODE_MT_node.remove(reset_nodes_button)
4823 for cls in classes:
4824 unregister_class(cls)
4826 if __name__ == "__main__":
4827 register()