Update for border select -> box select rename
[blender-addons.git] / node_wrangler.py
blob7d321f4863a8d80c5921b28aa57591b2c22d1142
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_offset[0] = 0
1566 context.space_data.backdrop_offset[1] = 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 bl_options = {'REGISTER', 'UNDO'}
1576 attr_name: StringProperty()
1578 def execute(self, context):
1579 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1580 nodes, links = get_nodes_links(context)
1581 nodes.active.attribute_name = self.attr_name
1582 return {'FINISHED'}
1585 class NWEmissionViewer(Operator, NWBase):
1586 bl_idname = "node.nw_emission_viewer"
1587 bl_label = "Emission Viewer"
1588 bl_description = "Connect active node to Emission Shader for shadeless previews"
1589 bl_options = {'REGISTER', 'UNDO'}
1591 @classmethod
1592 def poll(cls, context):
1593 is_cycles = is_cycles_or_eevee(context)
1594 if nw_check(context):
1595 space = context.space_data
1596 if space.tree_type == 'ShaderNodeTree' and is_cycles:
1597 if context.active_node:
1598 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1599 return True
1600 else:
1601 return True
1602 return False
1604 def invoke(self, context, event):
1605 space = context.space_data
1606 shader_type = space.shader_type
1607 if shader_type == 'OBJECT':
1608 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1609 shader_output_type = "OUTPUT_MATERIAL"
1610 shader_output_ident = "ShaderNodeOutputMaterial"
1611 shader_viewer_ident = "ShaderNodeEmission"
1612 else:
1613 shader_output_type = "OUTPUT_LIGHT"
1614 shader_output_ident = "ShaderNodeOutputLight"
1615 shader_viewer_ident = "ShaderNodeEmission"
1617 elif shader_type == 'WORLD':
1618 shader_output_type = "OUTPUT_WORLD"
1619 shader_output_ident = "ShaderNodeOutputWorld"
1620 shader_viewer_ident = "ShaderNodeBackground"
1621 shader_types = [x[1] for x in shaders_shader_nodes_props]
1622 mlocx = event.mouse_region_x
1623 mlocy = event.mouse_region_y
1624 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1625 if 'FINISHED' in select_node: # only run if mouse click is on a node
1626 nodes, links = get_nodes_links(context)
1627 in_group = context.active_node != space.node_tree.nodes.active
1628 active = nodes.active
1629 output_types = [x[1] for x in shaders_output_nodes_props]
1630 valid = False
1631 if active:
1632 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1633 for out in active.outputs:
1634 if not out.hide:
1635 valid = True
1636 break
1637 if valid:
1638 # get material_output node, store selection, deselect all
1639 materialout = None # placeholder node
1640 selection = []
1641 for node in nodes:
1642 if node.type == shader_output_type:
1643 materialout = node
1644 if node.select:
1645 selection.append(node.name)
1646 node.select = False
1647 if not materialout:
1648 # get right-most location
1649 sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
1650 max_xloc_node = sorted_by_xloc[-1]
1651 if max_xloc_node.name == 'Emission Viewer':
1652 max_xloc_node = sorted_by_xloc[-2]
1654 # get average y location
1655 sum_yloc = 0
1656 for node in nodes:
1657 sum_yloc += node.location.y
1659 new_locx = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1660 new_locy = sum_yloc / len(nodes)
1662 materialout = nodes.new(shader_output_ident)
1663 materialout.location.x = new_locx
1664 materialout.location.y = new_locy
1665 materialout.select = False
1666 # Analyze outputs, add "Emission Viewer" if needed, make links
1667 out_i = None
1668 valid_outputs = []
1669 for i, out in enumerate(active.outputs):
1670 if not out.hide:
1671 valid_outputs.append(i)
1672 if valid_outputs:
1673 out_i = valid_outputs[0] # Start index of node's outputs
1674 for i, valid_i in enumerate(valid_outputs):
1675 for out_link in active.outputs[valid_i].links:
1676 if "Emission Viewer" in out_link.to_node.name or (out_link.to_node == materialout and out_link.to_socket == materialout.inputs[0]):
1677 if i < len(valid_outputs) - 1:
1678 out_i = valid_outputs[i + 1]
1679 else:
1680 out_i = valid_outputs[0]
1681 make_links = [] # store sockets for new links
1682 if active.outputs:
1683 # If output type not 'SHADER' - "Emission Viewer" needed
1684 if active.outputs[out_i].type != 'SHADER':
1685 # get Emission Viewer node
1686 emission_exists = False
1687 emission_placeholder = nodes[0]
1688 for node in nodes:
1689 if "Emission Viewer" in node.name:
1690 emission_exists = True
1691 emission_placeholder = node
1692 if not emission_exists:
1693 emission = nodes.new(shader_viewer_ident)
1694 emission.hide = True
1695 emission.location = [materialout.location.x, (materialout.location.y + 40)]
1696 emission.label = "Viewer"
1697 emission.name = "Emission Viewer"
1698 emission.use_custom_color = True
1699 emission.color = (0.6, 0.5, 0.4)
1700 emission.select = False
1701 else:
1702 emission = emission_placeholder
1703 make_links.append((active.outputs[out_i], emission.inputs[0]))
1705 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
1706 if emission.outputs[0].links.__len__() > 0:
1707 if not emission.outputs[0].links[0].to_node == materialout:
1708 make_links.append((emission.outputs[0], materialout.inputs[0]))
1709 else:
1710 make_links.append((emission.outputs[0], materialout.inputs[0]))
1712 # Set brightness of viewer to compensate for Film and CM exposure
1713 intensity = 1/context.scene.cycles.film_exposure # Film exposure is a multiplier
1714 intensity /= pow(2, (context.scene.view_settings.exposure)) # CM exposure is measured in stops/EVs (2^x)
1715 emission.inputs[1].default_value = intensity
1717 else:
1718 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
1719 make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
1720 for node in nodes:
1721 if node.name == 'Emission Viewer':
1722 node.select = True
1723 bpy.ops.node.delete()
1724 for li_from, li_to in make_links:
1725 links.new(li_from, li_to)
1726 # Restore selection
1727 nodes.active = active
1728 for node in nodes:
1729 if node.name in selection:
1730 node.select = True
1731 force_update(context)
1732 return {'FINISHED'}
1733 else:
1734 return {'CANCELLED'}
1737 class NWFrameSelected(Operator, NWBase):
1738 bl_idname = "node.nw_frame_selected"
1739 bl_label = "Frame Selected"
1740 bl_description = "Add a frame node and parent the selected nodes to it"
1741 bl_options = {'REGISTER', 'UNDO'}
1743 label_prop: StringProperty(
1744 name='Label',
1745 description='The visual name of the frame node',
1746 default=' '
1748 color_prop: FloatVectorProperty(
1749 name="Color",
1750 description="The color of the frame node",
1751 default=(0.6, 0.6, 0.6),
1752 min=0, max=1, step=1, precision=3,
1753 subtype='COLOR_GAMMA', size=3
1756 def execute(self, context):
1757 nodes, links = get_nodes_links(context)
1758 selected = []
1759 for node in nodes:
1760 if node.select == True:
1761 selected.append(node)
1763 bpy.ops.node.add_node(type='NodeFrame')
1764 frm = nodes.active
1765 frm.label = self.label_prop
1766 frm.use_custom_color = True
1767 frm.color = self.color_prop
1769 for node in selected:
1770 node.parent = frm
1772 return {'FINISHED'}
1775 class NWReloadImages(Operator, NWBase):
1776 bl_idname = "node.nw_reload_images"
1777 bl_label = "Reload Images"
1778 bl_description = "Update all the image nodes to match their files on disk"
1780 def execute(self, context):
1781 nodes, links = get_nodes_links(context)
1782 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1783 num_reloaded = 0
1784 for node in nodes:
1785 if node.type in image_types:
1786 if node.type == "TEXTURE":
1787 if node.texture: # node has texture assigned
1788 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1789 if node.texture.image: # texture has image assigned
1790 node.texture.image.reload()
1791 num_reloaded += 1
1792 else:
1793 if node.image:
1794 node.image.reload()
1795 num_reloaded += 1
1797 if num_reloaded:
1798 self.report({'INFO'}, "Reloaded images")
1799 print("Reloaded " + str(num_reloaded) + " images")
1800 force_update(context)
1801 return {'FINISHED'}
1802 else:
1803 self.report({'WARNING'}, "No images found to reload in this node tree")
1804 return {'CANCELLED'}
1807 class NWSwitchNodeType(Operator, NWBase):
1808 """Switch type of selected nodes """
1809 bl_idname = "node.nw_swtch_node_type"
1810 bl_label = "Switch Node Type"
1811 bl_options = {'REGISTER', 'UNDO'}
1813 to_type: EnumProperty(
1814 name="Switch to type",
1815 items=list(shaders_input_nodes_props) +
1816 list(shaders_output_nodes_props) +
1817 list(shaders_shader_nodes_props) +
1818 list(shaders_texture_nodes_props) +
1819 list(shaders_color_nodes_props) +
1820 list(shaders_vector_nodes_props) +
1821 list(shaders_converter_nodes_props) +
1822 list(shaders_layout_nodes_props) +
1823 list(compo_input_nodes_props) +
1824 list(compo_output_nodes_props) +
1825 list(compo_color_nodes_props) +
1826 list(compo_converter_nodes_props) +
1827 list(compo_filter_nodes_props) +
1828 list(compo_vector_nodes_props) +
1829 list(compo_matte_nodes_props) +
1830 list(compo_distort_nodes_props) +
1831 list(compo_layout_nodes_props) +
1832 list(blender_mat_input_nodes_props) +
1833 list(blender_mat_output_nodes_props) +
1834 list(blender_mat_color_nodes_props) +
1835 list(blender_mat_vector_nodes_props) +
1836 list(blender_mat_converter_nodes_props) +
1837 list(blender_mat_layout_nodes_props) +
1838 list(texture_input_nodes_props) +
1839 list(texture_output_nodes_props) +
1840 list(texture_color_nodes_props) +
1841 list(texture_pattern_nodes_props) +
1842 list(texture_textures_nodes_props) +
1843 list(texture_converter_nodes_props) +
1844 list(texture_distort_nodes_props) +
1845 list(texture_layout_nodes_props)
1848 def execute(self, context):
1849 nodes, links = get_nodes_links(context)
1850 to_type = self.to_type
1851 # Those types of nodes will not swap.
1852 src_excludes = ('NodeFrame')
1853 # Those attributes of nodes will be copied if possible
1854 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1855 'show_options', 'show_preview', 'show_texture',
1856 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1858 selected = [n for n in nodes if n.select]
1859 reselect = []
1860 for node in [n for n in selected if
1861 n.rna_type.identifier not in src_excludes and
1862 n.rna_type.identifier != to_type]:
1863 new_node = nodes.new(to_type)
1864 for attr in attrs_to_pass:
1865 if hasattr(node, attr) and hasattr(new_node, attr):
1866 setattr(new_node, attr, getattr(node, attr))
1867 # set image datablock of dst to image of src
1868 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1869 if node.image:
1870 new_node.image = node.image
1871 # Special cases
1872 if new_node.type == 'SWITCH':
1873 new_node.hide = True
1874 # Dictionaries: src_sockets and dst_sockets:
1875 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1876 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1877 # in 'INPUTS' and 'OUTPUTS':
1878 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1879 # socket entry:
1880 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1881 src_sockets = {
1882 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1883 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1885 dst_sockets = {
1886 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1887 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1889 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1890 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1891 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1892 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1893 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1894 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1895 # enumerate in inputs, then in outputs
1896 # find name, default value and links of socket
1897 for i, socket in enumerate(in_out):
1898 the_name = socket.name
1899 dval = None
1900 # Not every socket, especially in outputs has "default_value"
1901 if hasattr(socket, 'default_value'):
1902 dval = socket.default_value
1903 socket_links = []
1904 for lnk in socket.links:
1905 socket_links.append(lnk)
1906 # check type of socket to fill proper keys.
1907 for the_type in types_order_one:
1908 if socket.type == the_type:
1909 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1910 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1911 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1912 # Check which of the types in inputs/outputs is considered to be "main".
1913 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1914 for type_check in types_order_one:
1915 if sockets[in_out_name][type_check]:
1916 sockets[in_out_name]['MAIN'] = type_check
1917 break
1919 matches = {
1920 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1921 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1924 for inout, soctype in (
1925 ('INPUTS', 'MAIN',),
1926 ('INPUTS', 'SHADER',),
1927 ('INPUTS', 'RGBA',),
1928 ('INPUTS', 'VECTOR',),
1929 ('INPUTS', 'VALUE',),
1930 ('OUTPUTS', 'MAIN',),
1931 ('OUTPUTS', 'SHADER',),
1932 ('OUTPUTS', 'RGBA',),
1933 ('OUTPUTS', 'VECTOR',),
1934 ('OUTPUTS', 'VALUE',),
1936 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1937 if soctype == 'MAIN':
1938 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1939 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1940 else:
1941 sc = src_sockets[inout][soctype]
1942 dt = dst_sockets[inout][soctype]
1943 # start with 'dt' to determine number of possibilities.
1944 for i, soc in enumerate(dt):
1945 # if src main has enough entries - match them with dst main sockets by indexes.
1946 if len(sc) > i:
1947 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1948 # add 'VALUE_NAME' criterion to inputs.
1949 if inout == 'INPUTS' and soctype == 'VALUE':
1950 for s in sc:
1951 if s[2] == soc[2]: # if names match
1952 # append src (index, dval), dst (index, dval)
1953 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1955 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1956 # This creates better links when relinking textures.
1957 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1958 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1960 # Pass default values and RELINK:
1961 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1962 # INPUTS: Base on matches in proper order.
1963 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1964 # pass dvals
1965 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1966 new_node.inputs[dst_i].default_value = src_dval
1967 # Special case: switch to math
1968 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1969 new_node.type == 'MATH' and\
1970 tp == 'MAIN':
1971 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1972 new_node.inputs[dst_i].default_value = new_dst_dval
1973 if node.type == 'MIX_RGB':
1974 if node.blend_type in [o[0] for o in operations]:
1975 new_node.operation = node.blend_type
1976 # Special case: switch from math to some types
1977 if node.type == 'MATH' and\
1978 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1979 tp == 'MAIN':
1980 for i in range(3):
1981 new_node.inputs[dst_i].default_value[i] = src_dval
1982 if new_node.type == 'MIX_RGB':
1983 if node.operation in [t[0] for t in blend_types]:
1984 new_node.blend_type = node.operation
1985 # Set Fac of MIX_RGB to 1.0
1986 new_node.inputs[0].default_value = 1.0
1987 # make link only when dst matching input is not linked already.
1988 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1989 in_src_link = node.inputs[src_i].links[0]
1990 in_dst_socket = new_node.inputs[dst_i]
1991 links.new(in_src_link.from_socket, in_dst_socket)
1992 links.remove(in_src_link)
1993 # OUTPUTS: Base on matches in proper order.
1994 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1995 for out_src_link in node.outputs[src_i].links:
1996 out_dst_socket = new_node.outputs[dst_i]
1997 links.new(out_dst_socket, out_src_link.to_socket)
1998 # relink rest inputs if possible, no criteria
1999 for src_inp in node.inputs:
2000 for dst_inp in new_node.inputs:
2001 if src_inp.links and not dst_inp.links:
2002 src_link = src_inp.links[0]
2003 links.new(src_link.from_socket, dst_inp)
2004 links.remove(src_link)
2005 # relink rest outputs if possible, base on node kind if any left.
2006 for src_o in node.outputs:
2007 for out_src_link in src_o.links:
2008 for dst_o in new_node.outputs:
2009 if src_o.type == dst_o.type:
2010 links.new(dst_o, out_src_link.to_socket)
2011 # relink rest outputs no criteria if any left. Link all from first output.
2012 for src_o in node.outputs:
2013 for out_src_link in src_o.links:
2014 if new_node.outputs:
2015 links.new(new_node.outputs[0], out_src_link.to_socket)
2016 nodes.remove(node)
2017 force_update(context)
2018 return {'FINISHED'}
2021 class NWMergeNodes(Operator, NWBase):
2022 bl_idname = "node.nw_merge_nodes"
2023 bl_label = "Merge Nodes"
2024 bl_description = "Merge Selected Nodes"
2025 bl_options = {'REGISTER', 'UNDO'}
2027 mode: EnumProperty(
2028 name="mode",
2029 description="All possible blend types and math operations",
2030 items=blend_types + [op for op in operations if op not in blend_types],
2032 merge_type: EnumProperty(
2033 name="merge type",
2034 description="Type of Merge to be used",
2035 items=(
2036 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2037 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2038 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2039 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2040 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2041 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2045 def execute(self, context):
2046 settings = context.user_preferences.addons[__name__].preferences
2047 merge_hide = settings.merge_hide
2048 merge_position = settings.merge_position # 'center' or 'bottom'
2050 do_hide = False
2051 do_hide_shader = False
2052 if merge_hide == 'ALWAYS':
2053 do_hide = True
2054 do_hide_shader = True
2055 elif merge_hide == 'NON_SHADER':
2056 do_hide = True
2058 tree_type = context.space_data.node_tree.type
2059 if tree_type == 'COMPOSITING':
2060 node_type = 'CompositorNode'
2061 elif tree_type == 'SHADER':
2062 node_type = 'ShaderNode'
2063 elif tree_type == 'TEXTURE':
2064 node_type = 'TextureNode'
2065 nodes, links = get_nodes_links(context)
2066 mode = self.mode
2067 merge_type = self.merge_type
2068 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2069 # 'ZCOMBINE' works only if mode == 'MIX'
2070 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2071 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2072 merge_type = 'MIX'
2073 mode = 'MIX'
2074 selected_mix = [] # entry = [index, loc]
2075 selected_shader = [] # entry = [index, loc]
2076 selected_math = [] # entry = [index, loc]
2077 selected_z = [] # entry = [index, loc]
2078 selected_alphaover = [] # entry = [index, loc]
2080 for i, node in enumerate(nodes):
2081 if node.select and node.outputs:
2082 if merge_type == 'AUTO':
2083 for (type, types_list, dst) in (
2084 ('SHADER', ('MIX', 'ADD'), selected_shader),
2085 ('RGBA', [t[0] for t in blend_types], selected_mix),
2086 ('VALUE', [t[0] for t in operations], selected_math),
2088 output_type = node.outputs[0].type
2089 valid_mode = mode in types_list
2090 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2091 # Cheat that output type is 'RGBA',
2092 # and that 'MIX' exists in math operations list.
2093 # This way when selected_mix list is analyzed:
2094 # Node data will be appended even though it doesn't meet requirements.
2095 if output_type != 'SHADER' and mode == 'MIX':
2096 output_type = 'RGBA'
2097 valid_mode = True
2098 if output_type == type and valid_mode:
2099 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2100 else:
2101 for (type, types_list, dst) in (
2102 ('SHADER', ('MIX', 'ADD'), selected_shader),
2103 ('MIX', [t[0] for t in blend_types], selected_mix),
2104 ('MATH', [t[0] for t in operations], selected_math),
2105 ('ZCOMBINE', ('MIX', ), selected_z),
2106 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2108 if merge_type == type and mode in types_list:
2109 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2110 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2111 # use only 'Mix' nodes for merging.
2112 # For that we add selected_math list to selected_mix list and clear selected_math.
2113 if selected_mix and selected_math and merge_type == 'AUTO':
2114 selected_mix += selected_math
2115 selected_math = []
2117 for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
2118 if nodes_list:
2119 count_before = len(nodes)
2120 # sort list by loc_x - reversed
2121 nodes_list.sort(key=lambda k: k[1], reverse=True)
2122 # get maximum loc_x
2123 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2124 nodes_list.sort(key=lambda k: k[2], reverse=True)
2125 if merge_position == 'CENTER':
2126 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)
2127 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2128 if do_hide:
2129 loc_y += 40
2130 else:
2131 loc_y += 80
2132 else:
2133 loc_y = nodes_list[len(nodes_list) - 1][2]
2134 offset_y = 100
2135 if not do_hide:
2136 offset_y = 200
2137 if nodes_list == selected_shader and not do_hide_shader:
2138 offset_y = 150.0
2139 the_range = len(nodes_list) - 1
2140 if len(nodes_list) == 1:
2141 the_range = 1
2142 for i in range(the_range):
2143 if nodes_list == selected_mix:
2144 add_type = node_type + 'MixRGB'
2145 add = nodes.new(add_type)
2146 add.blend_type = mode
2147 if mode != 'MIX':
2148 add.inputs[0].default_value = 1.0
2149 add.show_preview = False
2150 add.hide = do_hide
2151 if do_hide:
2152 loc_y = loc_y - 50
2153 first = 1
2154 second = 2
2155 add.width_hidden = 100.0
2156 elif nodes_list == selected_math:
2157 add_type = node_type + 'Math'
2158 add = nodes.new(add_type)
2159 add.operation = mode
2160 add.hide = do_hide
2161 if do_hide:
2162 loc_y = loc_y - 50
2163 first = 0
2164 second = 1
2165 add.width_hidden = 100.0
2166 elif nodes_list == selected_shader:
2167 if mode == 'MIX':
2168 add_type = node_type + 'MixShader'
2169 add = nodes.new(add_type)
2170 add.hide = do_hide_shader
2171 if do_hide_shader:
2172 loc_y = loc_y - 50
2173 first = 1
2174 second = 2
2175 add.width_hidden = 100.0
2176 elif mode == 'ADD':
2177 add_type = node_type + 'AddShader'
2178 add = nodes.new(add_type)
2179 add.hide = do_hide_shader
2180 if do_hide_shader:
2181 loc_y = loc_y - 50
2182 first = 0
2183 second = 1
2184 add.width_hidden = 100.0
2185 elif nodes_list == selected_z:
2186 add = nodes.new('CompositorNodeZcombine')
2187 add.show_preview = False
2188 add.hide = do_hide
2189 if do_hide:
2190 loc_y = loc_y - 50
2191 first = 0
2192 second = 2
2193 add.width_hidden = 100.0
2194 elif nodes_list == selected_alphaover:
2195 add = nodes.new('CompositorNodeAlphaOver')
2196 add.show_preview = False
2197 add.hide = do_hide
2198 if do_hide:
2199 loc_y = loc_y - 50
2200 first = 1
2201 second = 2
2202 add.width_hidden = 100.0
2203 add.location = loc_x, loc_y
2204 loc_y += offset_y
2205 add.select = True
2206 count_adds = i + 1
2207 count_after = len(nodes)
2208 index = count_after - 1
2209 first_selected = nodes[nodes_list[0][0]]
2210 # "last" node has been added as first, so its index is count_before.
2211 last_add = nodes[count_before]
2212 # Special case:
2213 # Two nodes were selected and first selected has no output links, second selected has output links.
2214 # Then add links from last add to all links 'to_socket' of out links of second selected.
2215 if len(nodes_list) == 2:
2216 if not first_selected.outputs[0].links:
2217 second_selected = nodes[nodes_list[1][0]]
2218 for ss_link in second_selected.outputs[0].links:
2219 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2220 # Create list of invalid indexes.
2221 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2222 # Link only if "to_node" index not in invalid indexes list.
2223 if ss_link.to_node not in [nodes[i] for i in invalid_i]:
2224 links.new(last_add.outputs[0], ss_link.to_socket)
2225 # add links from last_add to all links 'to_socket' of out links of first selected.
2226 for fs_link in first_selected.outputs[0].links:
2227 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2228 # Create list of invalid indexes.
2229 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2230 # Link only if "to_node" index not in invalid indexes list.
2231 if fs_link.to_node not in [nodes[i] for i in invalid_i]:
2232 links.new(last_add.outputs[0], fs_link.to_socket)
2233 # add link from "first" selected and "first" add node
2234 node_to = nodes[count_after - 1]
2235 links.new(first_selected.outputs[0], node_to.inputs[first])
2236 if node_to.type == 'ZCOMBINE':
2237 for fs_out in first_selected.outputs:
2238 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
2239 links.new(fs_out, node_to.inputs[1])
2240 break
2241 # add links between added ADD nodes and between selected and ADD nodes
2242 for i in range(count_adds):
2243 if i < count_adds - 1:
2244 node_from = nodes[index]
2245 node_to = nodes[index - 1]
2246 node_to_input_i = first
2247 node_to_z_i = 1 # if z combine - link z to first z input
2248 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2249 if node_to.type == 'ZCOMBINE':
2250 for from_out in node_from.outputs:
2251 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2252 links.new(from_out, node_to.inputs[node_to_z_i])
2253 if len(nodes_list) > 1:
2254 node_from = nodes[nodes_list[i + 1][0]]
2255 node_to = nodes[index]
2256 node_to_input_i = second
2257 node_to_z_i = 3 # if z combine - link z to second z input
2258 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2259 if node_to.type == 'ZCOMBINE':
2260 for from_out in node_from.outputs:
2261 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2262 links.new(from_out, node_to.inputs[node_to_z_i])
2263 index -= 1
2264 # set "last" of added nodes as active
2265 nodes.active = last_add
2266 for i, x, y, dx, h in nodes_list:
2267 nodes[i].select = False
2269 return {'FINISHED'}
2272 class NWBatchChangeNodes(Operator, NWBase):
2273 bl_idname = "node.nw_batch_change"
2274 bl_label = "Batch Change"
2275 bl_description = "Batch Change Blend Type and Math Operation"
2276 bl_options = {'REGISTER', 'UNDO'}
2278 blend_type: EnumProperty(
2279 name="Blend Type",
2280 items=blend_types + navs,
2282 operation: EnumProperty(
2283 name="Operation",
2284 items=operations + navs,
2287 def execute(self, context):
2289 nodes, links = get_nodes_links(context)
2290 blend_type = self.blend_type
2291 operation = self.operation
2292 for node in context.selected_nodes:
2293 if node.type == 'MIX_RGB':
2294 if not blend_type in [nav[0] for nav in navs]:
2295 node.blend_type = blend_type
2296 else:
2297 if blend_type == 'NEXT':
2298 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2299 #index = blend_types.index(node.blend_type)
2300 if index == len(blend_types) - 1:
2301 node.blend_type = blend_types[0][0]
2302 else:
2303 node.blend_type = blend_types[index + 1][0]
2305 if blend_type == 'PREV':
2306 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2307 if index == 0:
2308 node.blend_type = blend_types[len(blend_types) - 1][0]
2309 else:
2310 node.blend_type = blend_types[index - 1][0]
2312 if node.type == 'MATH':
2313 if not operation in [nav[0] for nav in navs]:
2314 node.operation = operation
2315 else:
2316 if operation == 'NEXT':
2317 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2318 #index = operations.index(node.operation)
2319 if index == len(operations) - 1:
2320 node.operation = operations[0][0]
2321 else:
2322 node.operation = operations[index + 1][0]
2324 if operation == 'PREV':
2325 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2326 #index = operations.index(node.operation)
2327 if index == 0:
2328 node.operation = operations[len(operations) - 1][0]
2329 else:
2330 node.operation = operations[index - 1][0]
2332 return {'FINISHED'}
2335 class NWChangeMixFactor(Operator, NWBase):
2336 bl_idname = "node.nw_factor"
2337 bl_label = "Change Factor"
2338 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2339 bl_options = {'REGISTER', 'UNDO'}
2341 # option: Change factor.
2342 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2343 # Else - change factor by option value.
2344 option: FloatProperty()
2346 def execute(self, context):
2347 nodes, links = get_nodes_links(context)
2348 option = self.option
2349 selected = [] # entry = index
2350 for si, node in enumerate(nodes):
2351 if node.select:
2352 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2353 selected.append(si)
2355 for si in selected:
2356 fac = nodes[si].inputs[0]
2357 nodes[si].hide = False
2358 if option in {0.0, 1.0}:
2359 fac.default_value = option
2360 else:
2361 fac.default_value += option
2363 return {'FINISHED'}
2366 class NWCopySettings(Operator, NWBase):
2367 bl_idname = "node.nw_copy_settings"
2368 bl_label = "Copy Settings"
2369 bl_description = "Copy Settings of Active Node to Selected Nodes"
2370 bl_options = {'REGISTER', 'UNDO'}
2372 @classmethod
2373 def poll(cls, context):
2374 valid = False
2375 if nw_check(context):
2376 if context.active_node is not None and context.active_node.type is not 'FRAME':
2377 valid = True
2378 return valid
2380 def execute(self, context):
2381 node_active = context.active_node
2382 node_selected = context.selected_nodes
2384 # Error handling
2385 if not (len(node_selected) > 1):
2386 self.report({'ERROR'}, "2 nodes must be selected at least")
2387 return {'CANCELLED'}
2389 # Check if active node is in the selection
2390 selected_node_names = [n.name for n in node_selected]
2391 if node_active.name not in selected_node_names:
2392 self.report({'ERROR'}, "No active node")
2393 return {'CANCELLED'}
2395 # Get nodes in selection by type
2396 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2398 if not (len(valid_nodes) > 1) and node_active:
2399 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2400 return {'CANCELLED'}
2402 if len(valid_nodes) != len(node_selected):
2403 # Report nodes that are not valid
2404 valid_node_names = [n.name for n in valid_nodes]
2405 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2406 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2408 # Reference original
2409 orig = node_active
2410 #node_selected_names = [n.name for n in node_selected]
2412 # Output list
2413 success_names = []
2415 # Deselect all nodes
2416 for i in node_selected:
2417 i.select = False
2419 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2420 # Run through all other nodes
2421 for node in valid_nodes[1:]:
2423 # Check for frame node
2424 parent = node.parent if node.parent else None
2425 node_loc = [node.location.x, node.location.y]
2427 # Select original to duplicate
2428 orig.select = True
2430 # Duplicate selected node
2431 bpy.ops.node.duplicate()
2432 new_node = context.selected_nodes[0]
2434 # Deselect copy
2435 new_node.select = False
2437 # Properties to copy
2438 node_tree = node.id_data
2439 props_to_copy = 'bl_idname name location height width'.split(' ')
2441 # Input and outputs
2442 reconnections = []
2443 mappings = chain.from_iterable([node.inputs, node.outputs])
2444 for i in (i for i in mappings if i.is_linked):
2445 for L in i.links:
2446 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2448 # Properties
2449 props = {j: getattr(node, j) for j in props_to_copy}
2450 props_to_copy.pop(0)
2452 for prop in props_to_copy:
2453 setattr(new_node, prop, props[prop])
2455 # Get the node tree to remove the old node
2456 nodes = node_tree.nodes
2457 nodes.remove(node)
2458 new_node.name = props['name']
2460 if parent:
2461 new_node.parent = parent
2462 new_node.location = node_loc
2464 for str_from, str_to in reconnections:
2465 node_tree.links.new(eval(str_from), eval(str_to))
2467 success_names.append(new_node.name)
2469 orig.select = True
2470 node_tree.nodes.active = orig
2471 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2472 return {'FINISHED'}
2475 class NWCopyLabel(Operator, NWBase):
2476 bl_idname = "node.nw_copy_label"
2477 bl_label = "Copy Label"
2478 bl_options = {'REGISTER', 'UNDO'}
2480 option: EnumProperty(
2481 name="option",
2482 description="Source of name of label",
2483 items=(
2484 ('FROM_ACTIVE', 'from active', 'from active node',),
2485 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2486 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2490 def execute(self, context):
2491 nodes, links = get_nodes_links(context)
2492 option = self.option
2493 active = nodes.active
2494 if option == 'FROM_ACTIVE':
2495 if active:
2496 src_label = active.label
2497 for node in [n for n in nodes if n.select and nodes.active != n]:
2498 node.label = src_label
2499 elif option == 'FROM_NODE':
2500 selected = [n for n in nodes if n.select]
2501 for node in selected:
2502 for input in node.inputs:
2503 if input.links:
2504 src = input.links[0].from_node
2505 node.label = src.label
2506 break
2507 elif option == 'FROM_SOCKET':
2508 selected = [n for n in nodes if n.select]
2509 for node in selected:
2510 for input in node.inputs:
2511 if input.links:
2512 src = input.links[0].from_socket
2513 node.label = src.name
2514 break
2516 return {'FINISHED'}
2519 class NWClearLabel(Operator, NWBase):
2520 bl_idname = "node.nw_clear_label"
2521 bl_label = "Clear Label"
2522 bl_options = {'REGISTER', 'UNDO'}
2524 option: BoolProperty()
2526 def execute(self, context):
2527 nodes, links = get_nodes_links(context)
2528 for node in [n for n in nodes if n.select]:
2529 node.label = ''
2531 return {'FINISHED'}
2533 def invoke(self, context, event):
2534 if self.option:
2535 return self.execute(context)
2536 else:
2537 return context.window_manager.invoke_confirm(self, event)
2540 class NWModifyLabels(Operator, NWBase):
2541 """Modify Labels of all selected nodes"""
2542 bl_idname = "node.nw_modify_labels"
2543 bl_label = "Modify Labels"
2544 bl_options = {'REGISTER', 'UNDO'}
2546 prepend: StringProperty(
2547 name="Add to Beginning"
2549 append: StringProperty(
2550 name="Add to End"
2552 replace_from: StringProperty(
2553 name="Text to Replace"
2555 replace_to: StringProperty(
2556 name="Replace with"
2559 def execute(self, context):
2560 nodes, links = get_nodes_links(context)
2561 for node in [n for n in nodes if n.select]:
2562 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2564 return {'FINISHED'}
2566 def invoke(self, context, event):
2567 self.prepend = ""
2568 self.append = ""
2569 self.remove = ""
2570 return context.window_manager.invoke_props_dialog(self)
2573 class NWAddTextureSetup(Operator, NWBase):
2574 bl_idname = "node.nw_add_texture"
2575 bl_label = "Texture Setup"
2576 bl_description = "Add Texture Node Setup to Selected Shaders"
2577 bl_options = {'REGISTER', 'UNDO'}
2579 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2581 @classmethod
2582 def poll(cls, context):
2583 valid = False
2584 if nw_check(context):
2585 space = context.space_data
2586 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2587 valid = True
2588 return valid
2590 def execute(self, context):
2591 nodes, links = get_nodes_links(context)
2592 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2593 texture_types = [x[1] for x in shaders_texture_nodes_props]
2594 selected_nodes = [n for n in nodes if n.select]
2595 for t_node in selected_nodes:
2596 valid = False
2597 input_index = 0
2598 if t_node.inputs:
2599 for index, i in enumerate(t_node.inputs):
2600 if not i.is_linked:
2601 valid = True
2602 input_index = index
2603 break
2604 if valid:
2605 locx = t_node.location.x
2606 locy = t_node.location.y - t_node.dimensions.y/2
2608 xoffset = [500, 700]
2609 is_texture = False
2610 if t_node.type in texture_types + ['MAPPING']:
2611 xoffset = [290, 500]
2612 is_texture = True
2614 coordout = 2
2615 image_type = 'ShaderNodeTexImage'
2617 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
2618 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2619 if t_node.type == 'BACKGROUND':
2620 image_type = 'ShaderNodeTexEnvironment'
2622 if not is_texture:
2623 tex = nodes.new(image_type)
2624 tex.location = [locx - 200, locy + 112]
2625 nodes.active = tex
2626 links.new(tex.outputs[0], t_node.inputs[input_index])
2628 t_node.select = False
2629 if self.add_mapping or is_texture:
2630 if t_node.type != 'MAPPING':
2631 m = nodes.new('ShaderNodeMapping')
2632 m.location = [locx - xoffset[0], locy + 141]
2633 m.width = 240
2634 else:
2635 m = t_node
2636 coord = nodes.new('ShaderNodeTexCoord')
2637 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
2639 if not is_texture:
2640 links.new(m.outputs[0], tex.inputs[0])
2641 links.new(coord.outputs[coordout], m.inputs[0])
2642 else:
2643 nodes.active = m
2644 links.new(m.outputs[0], t_node.inputs[input_index])
2645 links.new(coord.outputs[coordout], m.inputs[0])
2646 else:
2647 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
2648 return {'FINISHED'}
2651 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2652 bl_idname = "node.nw_add_textures_for_principled"
2653 bl_label = "Principled Texture Setup"
2654 bl_description = "Add Texture Node Setup for Principled BSDF"
2655 bl_options = {'REGISTER', 'UNDO'}
2657 directory: StringProperty(
2658 name='Directory',
2659 subtype='DIR_PATH',
2660 default='',
2661 description='Folder to search in for image files'
2663 files: CollectionProperty(
2664 type=bpy.types.OperatorFileListElement,
2665 options={'HIDDEN', 'SKIP_SAVE'}
2668 order = [
2669 "filepath",
2670 "files",
2673 @classmethod
2674 def poll(cls, context):
2675 valid = False
2676 if nw_check(context):
2677 space = context.space_data
2678 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2679 valid = True
2680 return valid
2682 def execute(self, context):
2683 # Check if everything is ok
2684 if not self.directory:
2685 self.report({'INFO'}, 'No Folder Selected')
2686 return {'CANCELLED'}
2687 if not self.files[:]:
2688 self.report({'INFO'}, 'No Files Selected')
2689 return {'CANCELLED'}
2691 nodes, links = get_nodes_links(context)
2692 active_node = nodes.active
2693 if not active_node.bl_idname == 'ShaderNodeBsdfPrincipled':
2694 self.report({'INFO'}, 'Select Principled BSDF')
2695 return {'CANCELLED'}
2697 # Helper_functions
2698 def split_into__components(fname):
2699 # Split filename into components
2700 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2701 # Remove extension
2702 fname = path.splitext(fname)[0]
2703 # Remove digits
2704 fname = ''.join(i for i in fname if not i.isdigit())
2705 # Seperate CamelCase by space
2706 fname = re.sub("([a-z])([A-Z])","\g<1> \g<2>",fname)
2707 # Replace common separators with SPACE
2708 seperators = ['_', '.', '-', '__', '--', '#']
2709 for sep in seperators:
2710 fname = fname.replace(sep, ' ')
2712 components = fname.split(' ')
2713 components = [c.lower() for c in components]
2714 return components
2716 # Filter textures names for texturetypes in filenames
2717 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2718 tags = context.user_preferences.addons[__name__].preferences.principled_tags
2719 normal_abbr = tags.normal.split(' ')
2720 bump_abbr = tags.bump.split(' ')
2721 gloss_abbr = tags.gloss.split(' ')
2722 rough_abbr = tags.rough.split(' ')
2723 socketnames = [
2724 ['Displacement', tags.displacement.split(' '), None],
2725 ['Base Color', tags.base_color.split(' '), None],
2726 ['Subsurface Color', tags.sss_color.split(' '), None],
2727 ['Metallic', tags.metallic.split(' '), None],
2728 ['Specular', tags.specular.split(' '), None],
2729 ['Roughness', rough_abbr + gloss_abbr, None],
2730 ['Normal', normal_abbr + bump_abbr, None],
2733 # Look through texture_types and set value as filename of first matched file
2734 def match_files_to_socket_names():
2735 for sname in socketnames:
2736 for file in self.files:
2737 fname = file.name
2738 filenamecomponents = split_into__components(fname)
2739 matches = set(sname[1]).intersection(set(filenamecomponents))
2740 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2741 if matches:
2742 sname[2] = fname
2743 break
2745 match_files_to_socket_names()
2746 # Remove socketnames without found files
2747 socketnames = [s for s in socketnames if s[2]
2748 and path.exists(self.directory+s[2])]
2749 if not socketnames:
2750 self.report({'INFO'}, 'No matching images found')
2751 print('No matching images found')
2752 return {'CANCELLED'}
2754 # Add found images
2755 print('\nMatched Textures:')
2756 texture_nodes = []
2757 disp_texture = None
2758 normal_node = None
2759 roughness_node = None
2760 for i, sname in enumerate(socketnames):
2761 print(i, sname[0], sname[2])
2763 # DISPLACEMENT NODES
2764 if sname[0] == 'Displacement':
2765 disp_texture = nodes.new(type='ShaderNodeTexImage')
2766 img = bpy.data.images.load(self.directory+sname[2])
2767 disp_texture.image = img
2768 disp_texture.label = 'Displacement'
2769 disp_texture.color_space = 'NONE'
2771 # Add displacement offset nodes
2772 math_sub = nodes.new(type='ShaderNodeMath')
2773 math_sub.operation = 'SUBTRACT'
2774 math_sub.label = 'Offset'
2775 math_sub.location = active_node.location + Vector((0, -560))
2776 math_mul = nodes.new(type='ShaderNodeMath')
2777 math_mul.operation = 'MULTIPLY'
2778 math_mul.label = 'Strength'
2779 math_mul.location = math_sub.location + Vector((200, 0))
2780 link = links.new(math_mul.inputs[0], math_sub.outputs[0])
2781 link = links.new(math_sub.inputs[0], disp_texture.outputs[0])
2783 # Turn on true displacement in the material
2784 # Too complicated for now
2787 # Frame. Does not update immediatly
2788 # Seems to need an editor redraw
2789 frame = nodes.new(type='NodeFrame')
2790 frame.label = 'Displacement'
2791 math_sub.parent = frame
2792 math_mul.parent = frame
2793 frame.update()
2796 #find ouput node
2797 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2798 if output_node:
2799 if not output_node[0].inputs[2].is_linked:
2800 link = links.new(output_node[0].inputs[2], math_mul.outputs[0])
2802 continue
2804 if not active_node.inputs[sname[0]].is_linked:
2805 # No texture node connected -> add texture node with new image
2806 texture_node = nodes.new(type='ShaderNodeTexImage')
2807 img = bpy.data.images.load(self.directory+sname[2])
2808 texture_node.image = img
2810 # NORMAL NODES
2811 if sname[0] == 'Normal':
2812 # Test if new texture node is normal or bump map
2813 fname_components = split_into__components(sname[2])
2814 match_normal = set(normal_abbr).intersection(set(fname_components))
2815 match_bump = set(bump_abbr).intersection(set(fname_components))
2816 if match_normal:
2817 # If Normal add normal node in between
2818 normal_node = nodes.new(type='ShaderNodeNormalMap')
2819 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
2820 elif match_bump:
2821 # If Bump add bump node in between
2822 normal_node = nodes.new(type='ShaderNodeBump')
2823 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
2825 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
2826 normal_node_texture = texture_node
2828 elif sname[0] == 'Roughness':
2829 # Test if glossy or roughness map
2830 fname_components = split_into__components(sname[2])
2831 match_rough = set(rough_abbr).intersection(set(fname_components))
2832 match_gloss = set(gloss_abbr).intersection(set(fname_components))
2834 if match_rough:
2835 # If Roughness nothing to to
2836 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2838 elif match_gloss:
2839 # If Gloss Map add invert node
2840 invert_node = nodes.new(type='ShaderNodeInvert')
2841 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
2843 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
2844 roughness_node = texture_node
2846 else:
2847 # This is a simple connection Texture --> Input slot
2848 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2850 # Use non-color for all but 'Base Color' Textures
2851 if not sname[0] in ['Base Color']:
2852 texture_node.color_space = 'NONE'
2854 else:
2855 # If already texture connected. add to node list for alignment
2856 texture_node = active_node.inputs[sname[0]].links[0].from_node
2858 # This are all connected texture nodes
2859 texture_nodes.append(texture_node)
2860 texture_node.label = sname[0]
2862 if disp_texture:
2863 texture_nodes.append(disp_texture)
2865 # Alignment
2866 for i, texture_node in enumerate(texture_nodes):
2867 offset = Vector((-400, (i * -260) + 200))
2868 texture_node.location = active_node.location + offset
2870 if normal_node:
2871 # Extra alignment if normal node was added
2872 normal_node.location = normal_node_texture.location + Vector((200, 0))
2874 if roughness_node:
2875 # Alignment of invert node if glossy map
2876 invert_node.location = roughness_node.location + Vector((200, 0))
2878 # Add texture input + mapping
2879 mapping = nodes.new(type='ShaderNodeMapping')
2880 mapping.location = active_node.location + Vector((-900, 0))
2881 if len(texture_nodes) > 1:
2882 # If more than one texture add reroute node in between
2883 reroute = nodes.new(type='NodeReroute')
2884 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
2885 reroute.location = tex_coords + Vector((-50, -120))
2886 for texture_node in texture_nodes:
2887 link = links.new(texture_node.inputs[0], reroute.outputs[0])
2888 link = links.new(reroute.inputs[0], mapping.outputs[0])
2889 else:
2890 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
2892 # Connect texture_coordiantes to mapping node
2893 texture_input = nodes.new(type='ShaderNodeTexCoord')
2894 texture_input.location = mapping.location + Vector((-200, 0))
2895 link = links.new(mapping.inputs[0], texture_input.outputs[2])
2897 # Just to be sure
2898 active_node.select = False
2899 nodes.update()
2900 links.update()
2901 force_update(context)
2902 return {'FINISHED'}
2905 class NWAddReroutes(Operator, NWBase):
2906 """Add Reroute Nodes and link them to outputs of selected nodes"""
2907 bl_idname = "node.nw_add_reroutes"
2908 bl_label = "Add Reroutes"
2909 bl_description = "Add Reroutes to Outputs"
2910 bl_options = {'REGISTER', 'UNDO'}
2912 option: EnumProperty(
2913 name="option",
2914 items=[
2915 ('ALL', 'to all', 'Add to all outputs'),
2916 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2917 ('LINKED', 'to linked', 'Add only to linked outputs'),
2921 def execute(self, context):
2922 tree_type = context.space_data.node_tree.type
2923 option = self.option
2924 nodes, links = get_nodes_links(context)
2925 # output valid when option is 'all' or when 'loose' output has no links
2926 valid = False
2927 post_select = [] # nodes to be selected after execution
2928 # create reroutes and recreate links
2929 for node in [n for n in nodes if n.select]:
2930 if node.outputs:
2931 x = node.location.x
2932 y = node.location.y
2933 width = node.width
2934 # unhide 'REROUTE' nodes to avoid issues with location.y
2935 if node.type == 'REROUTE':
2936 node.hide = False
2937 # When node is hidden - width_hidden not usable.
2938 # Hack needed to calculate real width
2939 if node.hide:
2940 bpy.ops.node.select_all(action='DESELECT')
2941 helper = nodes.new('NodeReroute')
2942 helper.select = True
2943 node.select = True
2944 # resize node and helper to zero. Then check locations to calculate width
2945 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2946 width = 2.0 * (helper.location.x - node.location.x)
2947 # restore node location
2948 node.location = x, y
2949 # delete helper
2950 node.select = False
2951 # only helper is selected now
2952 bpy.ops.node.delete()
2953 x = node.location.x + width + 20.0
2954 if node.type != 'REROUTE':
2955 y -= 35.0
2956 y_offset = -22.0
2957 loc = x, y
2958 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2959 for out_i, output in enumerate(node.outputs):
2960 pass_used = False # initial value to be analyzed if 'R_LAYERS'
2961 # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True
2962 if node.type != 'R_LAYERS':
2963 pass_used = True
2964 else: # if 'R_LAYERS' check if output represent used render pass
2965 node_scene = node.scene
2966 node_layer = node.layer
2967 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2968 if output.name == 'Alpha':
2969 pass_used = True
2970 else:
2971 # check entries in global 'rl_outputs' variable
2972 #for render_pass, output_name, exr_name, in_internal, in_cycles in rl_outputs:
2973 for rlo in rl_outputs:
2974 if output.name == rlo.output_name or output.name == rlo.exr_output_name:
2975 pass_used = getattr(node_scene.render.layers[node_layer], rlo.render_pass)
2976 break
2977 if pass_used:
2978 valid = ((option == 'ALL') or
2979 (option == 'LOOSE' and not output.links) or
2980 (option == 'LINKED' and output.links))
2981 # Add reroutes only if valid, but offset location in all cases.
2982 if valid:
2983 n = nodes.new('NodeReroute')
2984 nodes.active = n
2985 for link in output.links:
2986 links.new(n.outputs[0], link.to_socket)
2987 links.new(output, n.inputs[0])
2988 n.location = loc
2989 post_select.append(n)
2990 reroutes_count += 1
2991 y += y_offset
2992 loc = x, y
2993 # disselect the node so that after execution of script only newly created nodes are selected
2994 node.select = False
2995 # nicer reroutes distribution along y when node.hide
2996 if node.hide:
2997 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2998 for reroute in [r for r in nodes if r.select]:
2999 reroute.location.y -= y_translate
3000 for node in post_select:
3001 node.select = True
3003 return {'FINISHED'}
3006 class NWLinkActiveToSelected(Operator, NWBase):
3007 """Link active node to selected nodes basing on various criteria"""
3008 bl_idname = "node.nw_link_active_to_selected"
3009 bl_label = "Link Active Node to Selected"
3010 bl_options = {'REGISTER', 'UNDO'}
3012 replace: BoolProperty()
3013 use_node_name: BoolProperty()
3014 use_outputs_names: BoolProperty()
3016 @classmethod
3017 def poll(cls, context):
3018 valid = False
3019 if nw_check(context):
3020 if context.active_node is not None:
3021 if context.active_node.select:
3022 valid = True
3023 return valid
3025 def execute(self, context):
3026 nodes, links = get_nodes_links(context)
3027 replace = self.replace
3028 use_node_name = self.use_node_name
3029 use_outputs_names = self.use_outputs_names
3030 active = nodes.active
3031 selected = [node for node in nodes if node.select and node != active]
3032 outputs = [] # Only usable outputs of active nodes will be stored here.
3033 for out in active.outputs:
3034 if active.type != 'R_LAYERS':
3035 outputs.append(out)
3036 else:
3037 # 'R_LAYERS' node type needs special handling.
3038 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3039 # Only outputs that represent used passes should be taken into account
3040 # Check if pass represented by output is used.
3041 # global 'rl_outputs' list will be used for that
3042 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
3043 pass_used = False # initial value. Will be set to True if pass is used
3044 if out.name == 'Alpha':
3045 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3046 pass_used = True
3047 elif out.name == out_name:
3048 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3049 pass_used = getattr(active.scene.render.layers[active.layer], render_pass)
3050 break
3051 if pass_used:
3052 outputs.append(out)
3053 doit = True # Will be changed to False when links successfully added to previous output.
3054 for out in outputs:
3055 if doit:
3056 for node in selected:
3057 dst_name = node.name # Will be compared with src_name if needed.
3058 # When node has label - use it as dst_name
3059 if node.label:
3060 dst_name = node.label
3061 valid = True # Initial value. Will be changed to False if names don't match.
3062 src_name = dst_name # If names not used - this asignment will keep valid = True.
3063 if use_node_name:
3064 # Set src_name to source node name or label
3065 src_name = active.name
3066 if active.label:
3067 src_name = active.label
3068 elif use_outputs_names:
3069 src_name = (out.name, )
3070 for render_pass, out_name, exr_name, in_internal, in_cycles in rl_outputs:
3071 if out.name in {out_name, exr_name}:
3072 src_name = (out_name, exr_name)
3073 if dst_name not in src_name:
3074 valid = False
3075 if valid:
3076 for input in node.inputs:
3077 if input.type == out.type or node.type == 'REROUTE':
3078 if replace or not input.is_linked:
3079 links.new(out, input)
3080 if not use_node_name and not use_outputs_names:
3081 doit = False
3082 break
3084 return {'FINISHED'}
3087 class NWAlignNodes(Operator, NWBase):
3088 '''Align the selected nodes neatly in a row/column'''
3089 bl_idname = "node.nw_align_nodes"
3090 bl_label = "Align Nodes"
3091 bl_options = {'REGISTER', 'UNDO'}
3092 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3094 def execute(self, context):
3095 nodes, links = get_nodes_links(context)
3096 margin = self.margin
3098 selection = []
3099 for node in nodes:
3100 if node.select and node.type != 'FRAME':
3101 selection.append(node)
3103 # If no nodes are selected, align all nodes
3104 active_loc = None
3105 if not selection:
3106 selection = nodes
3107 elif nodes.active in selection:
3108 active_loc = copy(nodes.active.location) # make a copy, not a reference
3110 # Check if nodes should be layed out horizontally or vertically
3111 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3112 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3113 x_range = max(x_locs) - min(x_locs)
3114 y_range = max(y_locs) - min(y_locs)
3115 mid_x = (max(x_locs) + min(x_locs)) / 2
3116 mid_y = (max(y_locs) + min(y_locs)) / 2
3117 horizontal = x_range > y_range
3119 # Sort selection by location of node mid-point
3120 if horizontal:
3121 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3122 else:
3123 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3125 # Alignment
3126 current_pos = 0
3127 for node in selection:
3128 current_margin = margin
3129 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3131 if horizontal:
3132 node.location.x = current_pos
3133 current_pos += current_margin + node.dimensions.x
3134 node.location.y = mid_y + (node.dimensions.y / 2)
3135 else:
3136 node.location.y = current_pos
3137 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3138 node.location.x = mid_x - (node.dimensions.x / 2)
3140 # If active node is selected, center nodes around it
3141 if active_loc is not None:
3142 active_loc_diff = active_loc - nodes.active.location
3143 for node in selection:
3144 node.location += active_loc_diff
3145 else: # Position nodes centered around where they used to be
3146 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])
3147 new_mid = (max(locs) + min(locs)) / 2
3148 for node in selection:
3149 if horizontal:
3150 node.location.x += (mid_x - new_mid)
3151 else:
3152 node.location.y += (mid_y - new_mid)
3154 return {'FINISHED'}
3157 class NWSelectParentChildren(Operator, NWBase):
3158 bl_idname = "node.nw_select_parent_child"
3159 bl_label = "Select Parent or Children"
3160 bl_options = {'REGISTER', 'UNDO'}
3162 option: EnumProperty(
3163 name="option",
3164 items=(
3165 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3166 ('CHILD', 'Select Children', 'Select members of selected frame'),
3170 def execute(self, context):
3171 nodes, links = get_nodes_links(context)
3172 option = self.option
3173 selected = [node for node in nodes if node.select]
3174 if option == 'PARENT':
3175 for sel in selected:
3176 parent = sel.parent
3177 if parent:
3178 parent.select = True
3179 else: # option == 'CHILD'
3180 for sel in selected:
3181 children = [node for node in nodes if node.parent == sel]
3182 for kid in children:
3183 kid.select = True
3185 return {'FINISHED'}
3188 class NWDetachOutputs(Operator, NWBase):
3189 """Detach outputs of selected node leaving inputs linked"""
3190 bl_idname = "node.nw_detach_outputs"
3191 bl_label = "Detach Outputs"
3192 bl_options = {'REGISTER', 'UNDO'}
3194 def execute(self, context):
3195 nodes, links = get_nodes_links(context)
3196 selected = context.selected_nodes
3197 bpy.ops.node.duplicate_move_keep_inputs()
3198 new_nodes = context.selected_nodes
3199 bpy.ops.node.select_all(action="DESELECT")
3200 for node in selected:
3201 node.select = True
3202 bpy.ops.node.delete_reconnect()
3203 for new_node in new_nodes:
3204 new_node.select = True
3205 bpy.ops.transform.translate('INVOKE_DEFAULT')
3207 return {'FINISHED'}
3210 class NWLinkToOutputNode(Operator, NWBase):
3211 """Link to Composite node or Material Output node"""
3212 bl_idname = "node.nw_link_out"
3213 bl_label = "Connect to Output"
3214 bl_options = {'REGISTER', 'UNDO'}
3216 @classmethod
3217 def poll(cls, context):
3218 valid = False
3219 if nw_check(context):
3220 if context.active_node is not None:
3221 for out in context.active_node.outputs:
3222 if not out.hide:
3223 valid = True
3224 break
3225 return valid
3227 def execute(self, context):
3228 nodes, links = get_nodes_links(context)
3229 active = nodes.active
3230 output_node = None
3231 output_index = None
3232 tree_type = context.space_data.tree_type
3233 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
3234 output_types_compo = ['COMPOSITE']
3235 output_types_blender_mat = ['OUTPUT']
3236 output_types_textures = ['OUTPUT']
3237 output_types = output_types_shaders + output_types_compo + output_types_blender_mat
3238 for node in nodes:
3239 if node.type in output_types:
3240 output_node = node
3241 break
3242 if not output_node:
3243 bpy.ops.node.select_all(action="DESELECT")
3244 if tree_type == 'ShaderNodeTree':
3245 if is_cycles_or_eevee(context):
3246 output_node = nodes.new('ShaderNodeOutputMaterial')
3247 else:
3248 output_node = nodes.new('ShaderNodeOutput')
3249 elif tree_type == 'CompositorNodeTree':
3250 output_node = nodes.new('CompositorNodeComposite')
3251 elif tree_type == 'TextureNodeTree':
3252 output_node = nodes.new('TextureNodeOutput')
3253 output_node.location.x = active.location.x + active.dimensions.x + 80
3254 output_node.location.y = active.location.y
3255 if (output_node and active.outputs):
3256 for i, output in enumerate(active.outputs):
3257 if not output.hide:
3258 output_index = i
3259 break
3260 for i, output in enumerate(active.outputs):
3261 if output.type == output_node.inputs[0].type and not output.hide:
3262 output_index = i
3263 break
3265 out_input_index = 0
3266 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3267 if active.outputs[output_index].name == 'Volume':
3268 out_input_index = 1
3269 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3270 out_input_index = 2
3271 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3273 force_update(context) # viewport render does not update
3275 return {'FINISHED'}
3278 class NWMakeLink(Operator, NWBase):
3279 """Make a link from one socket to another"""
3280 bl_idname = 'node.nw_make_link'
3281 bl_label = 'Make Link'
3282 bl_options = {'REGISTER', 'UNDO'}
3283 from_socket: IntProperty()
3284 to_socket: IntProperty()
3286 def execute(self, context):
3287 nodes, links = get_nodes_links(context)
3289 n1 = nodes[context.scene.NWLazySource]
3290 n2 = nodes[context.scene.NWLazyTarget]
3292 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3294 force_update(context)
3296 return {'FINISHED'}
3299 class NWCallInputsMenu(Operator, NWBase):
3300 """Link from this output"""
3301 bl_idname = 'node.nw_call_inputs_menu'
3302 bl_label = 'Make Link'
3303 bl_options = {'REGISTER', 'UNDO'}
3304 from_socket: IntProperty()
3306 def execute(self, context):
3307 nodes, links = get_nodes_links(context)
3309 context.scene.NWSourceSocket = self.from_socket
3311 n1 = nodes[context.scene.NWLazySource]
3312 n2 = nodes[context.scene.NWLazyTarget]
3313 if len(n2.inputs) > 1:
3314 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3315 elif len(n2.inputs) == 1:
3316 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3317 return {'FINISHED'}
3320 class NWAddSequence(Operator, ImportHelper):
3321 """Add an Image Sequence"""
3322 bl_idname = 'node.nw_add_sequence'
3323 bl_label = 'Import Image Sequence'
3324 bl_options = {'REGISTER', 'UNDO'}
3326 directory: StringProperty(
3327 subtype="DIR_PATH"
3329 filename: StringProperty(
3330 subtype="FILE_NAME"
3332 files: CollectionProperty(
3333 type=bpy.types.OperatorFileListElement,
3334 options={'HIDDEN', 'SKIP_SAVE'}
3337 def execute(self, context):
3338 nodes, links = get_nodes_links(context)
3339 directory = self.directory
3340 filename = self.filename
3341 files = self.files
3342 tree = context.space_data.node_tree
3344 # DEBUG
3345 # print ("\nDIR:", directory)
3346 # print ("FN:", filename)
3347 # print ("Fs:", list(f.name for f in files), '\n')
3349 if tree.type == 'SHADER':
3350 node_type = "ShaderNodeTexImage"
3351 elif tree.type == 'COMPOSITING':
3352 node_type = "CompositorNodeImage"
3353 else:
3354 self.report({'ERROR'}, "Unsupported Node Tree type!")
3355 return {'CANCELLED'}
3357 if not files[0].name and not filename:
3358 self.report({'ERROR'}, "No file chosen")
3359 return {'CANCELLED'}
3360 elif files[0].name and (not filename or not path.exists(directory+filename)):
3361 # User has selected multiple files without an active one, or the active one is non-existant
3362 filename = files[0].name
3364 if not path.exists(directory+filename):
3365 self.report({'ERROR'}, filename+" does not exist!")
3366 return {'CANCELLED'}
3368 without_ext = '.'.join(filename.split('.')[:-1])
3370 # if last digit isn't a number, it's not a sequence
3371 if not without_ext[-1].isdigit():
3372 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3373 return {'CANCELLED'}
3376 extension = filename.split('.')[-1]
3377 reverse = without_ext[::-1] # reverse string
3379 count_numbers = 0
3380 for char in reverse:
3381 if char.isdigit():
3382 count_numbers += 1
3383 else:
3384 break
3386 without_num = without_ext[:count_numbers*-1]
3388 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3390 num_frames = len(files)
3392 nodes_list = [node for node in nodes]
3393 if nodes_list:
3394 nodes_list.sort(key=lambda k: k.location.x)
3395 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3396 yloc = 0
3397 for node in nodes:
3398 node.select = False
3399 yloc += node_mid_pt(node, 'y')
3400 yloc = yloc/len(nodes)
3401 else:
3402 xloc = 0
3403 yloc = 0
3405 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3407 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3408 node = nodes.active
3409 node.label = name_with_hashes
3411 img = bpy.data.images.load(directory+(without_ext+'.'+extension))
3412 img.source = 'SEQUENCE'
3413 img.name = name_with_hashes
3414 node.image = img
3415 image_user = node.image_user if tree.type == 'SHADER' else node
3416 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
3417 image_user.frame_duration = num_frames
3419 return {'FINISHED'}
3422 class NWAddMultipleImages(Operator, ImportHelper):
3423 """Add multiple images at once"""
3424 bl_idname = 'node.nw_add_multiple_images'
3425 bl_label = 'Open Selected Images'
3426 bl_options = {'REGISTER', 'UNDO'}
3427 directory: StringProperty(
3428 subtype="DIR_PATH"
3430 files: CollectionProperty(
3431 type=bpy.types.OperatorFileListElement,
3432 options={'HIDDEN', 'SKIP_SAVE'}
3435 def execute(self, context):
3436 nodes, links = get_nodes_links(context)
3438 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3440 if context.space_data.node_tree.type == 'SHADER':
3441 node_type = "ShaderNodeTexImage"
3442 elif context.space_data.node_tree.type == 'COMPOSITING':
3443 node_type = "CompositorNodeImage"
3444 else:
3445 self.report({'ERROR'}, "Unsupported Node Tree type!")
3446 return {'CANCELLED'}
3448 new_nodes = []
3449 for f in self.files:
3450 fname = f.name
3452 node = nodes.new(node_type)
3453 new_nodes.append(node)
3454 node.label = fname
3455 node.hide = True
3456 node.width_hidden = 100
3457 node.location.x = xloc
3458 node.location.y = yloc
3459 yloc -= 40
3461 img = bpy.data.images.load(self.directory+fname)
3462 node.image = img
3464 # shift new nodes up to center of tree
3465 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3466 for node in nodes:
3467 if node in new_nodes:
3468 node.select = True
3469 node.location.y += (list_size/2)
3470 else:
3471 node.select = False
3472 return {'FINISHED'}
3475 class NWViewerFocus(bpy.types.Operator):
3476 """Set the viewer tile center to the mouse position"""
3477 bl_idname = "node.nw_viewer_focus"
3478 bl_label = "Viewer Focus"
3480 x: bpy.props.IntProperty()
3481 y: bpy.props.IntProperty()
3483 @classmethod
3484 def poll(cls, context):
3485 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3487 def execute(self, context):
3488 return {'FINISHED'}
3490 def invoke(self, context, event):
3491 render = context.scene.render
3492 space = context.space_data
3493 percent = render.resolution_percentage*0.01
3495 nodes, links = get_nodes_links(context)
3496 viewers = [n for n in nodes if n.type == 'VIEWER']
3498 if viewers:
3499 mlocx = event.mouse_region_x
3500 mlocy = event.mouse_region_y
3501 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
3503 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3504 region_x = context.region.width
3505 region_y = context.region.height
3507 region_center_x = context.region.width / 2
3508 region_center_y = context.region.height / 2
3510 bd_x = render.resolution_x * percent * space.backdrop_zoom
3511 bd_y = render.resolution_y * percent * space.backdrop_zoom
3513 backdrop_center_x = (bd_x / 2) - space.backdrop_x
3514 backdrop_center_y = (bd_y / 2) - space.backdrop_y
3516 margin_x = region_center_x - backdrop_center_x
3517 margin_y = region_center_y - backdrop_center_y
3519 abs_mouse_x = (mlocx - margin_x) / bd_x
3520 abs_mouse_y = (mlocy - margin_y) / bd_y
3522 for node in viewers:
3523 node.center_x = abs_mouse_x
3524 node.center_y = abs_mouse_y
3525 else:
3526 return {'PASS_THROUGH'}
3528 return self.execute(context)
3531 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3532 """Save the current viewer node to an image file"""
3533 bl_idname = "node.nw_save_viewer"
3534 bl_label = "Save This Image"
3535 filepath: StringProperty(subtype="FILE_PATH")
3536 filename_ext: EnumProperty(
3537 name="Format",
3538 description="Choose the file format to save to",
3539 items=(('.bmp', "PNG", ""),
3540 ('.rgb', 'IRIS', ""),
3541 ('.png', 'PNG', ""),
3542 ('.jpg', 'JPEG', ""),
3543 ('.jp2', 'JPEG2000', ""),
3544 ('.tga', 'TARGA', ""),
3545 ('.cin', 'CINEON', ""),
3546 ('.dpx', 'DPX', ""),
3547 ('.exr', 'OPEN_EXR', ""),
3548 ('.hdr', 'HDR', ""),
3549 ('.tif', 'TIFF', "")),
3550 default='.png',
3553 @classmethod
3554 def poll(cls, context):
3555 valid = False
3556 if nw_check(context):
3557 if context.space_data.tree_type == 'CompositorNodeTree':
3558 if "Viewer Node" in [i.name for i in bpy.data.images]:
3559 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3560 valid = True
3561 return valid
3563 def execute(self, context):
3564 fp = self.filepath
3565 if fp:
3566 formats = {
3567 '.bmp': 'BMP',
3568 '.rgb': 'IRIS',
3569 '.png': 'PNG',
3570 '.jpg': 'JPEG',
3571 '.jpeg': 'JPEG',
3572 '.jp2': 'JPEG2000',
3573 '.tga': 'TARGA',
3574 '.cin': 'CINEON',
3575 '.dpx': 'DPX',
3576 '.exr': 'OPEN_EXR',
3577 '.hdr': 'HDR',
3578 '.tiff': 'TIFF',
3579 '.tif': 'TIFF'}
3580 basename, ext = path.splitext(fp)
3581 old_render_format = context.scene.render.image_settings.file_format
3582 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3583 context.area.type = "IMAGE_EDITOR"
3584 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3585 context.area.spaces[0].image.save_render(fp)
3586 context.area.type = "NODE_EDITOR"
3587 context.scene.render.image_settings.file_format = old_render_format
3588 return {'FINISHED'}
3591 class NWResetNodes(bpy.types.Operator):
3592 """Reset Nodes in Selection"""
3593 bl_idname = "node.nw_reset_nodes"
3594 bl_label = "Reset Nodes"
3595 bl_options = {'REGISTER', 'UNDO'}
3597 @classmethod
3598 def poll(cls, context):
3599 space = context.space_data
3600 return space.type == 'NODE_EDITOR'
3602 def execute(self, context):
3603 node_active = context.active_node
3604 node_selected = context.selected_nodes
3605 node_ignore = ["FRAME","REROUTE", "GROUP"]
3607 # Check if one node is selected at least
3608 if not (len(node_selected) > 0):
3609 self.report({'ERROR'}, "1 node must be selected at least")
3610 return {'CANCELLED'}
3612 active_node_name = node_active.name if node_active.select else None
3613 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3615 # Create output lists
3616 selected_node_names = [n.name for n in node_selected]
3617 success_names = []
3619 # Reset all valid children in a frame
3620 node_active_is_frame = False
3621 if len(node_selected) == 1 and node_active.type == "FRAME":
3622 node_tree = node_active.id_data
3623 children = [n for n in node_tree.nodes if n.parent == node_active]
3624 if children:
3625 valid_nodes = [n for n in children if n.type not in node_ignore]
3626 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3627 node_active_is_frame = True
3629 # Check if valid nodes in selection
3630 if not (len(valid_nodes) > 0):
3631 # Check for frames only
3632 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3633 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3634 self.report({'ERROR'}, "Please select only 1 frame to reset")
3635 else:
3636 self.report({'ERROR'}, "No valid node(s) in selection")
3637 return {'CANCELLED'}
3639 # Report nodes that are not valid
3640 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3641 valid_node_names = [n.name for n in valid_nodes]
3642 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3643 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3645 # Deselect all nodes
3646 for i in node_selected:
3647 i.select = False
3649 # Run through all valid nodes
3650 for node in valid_nodes:
3652 parent = node.parent if node.parent else None
3653 node_loc = [node.location.x, node.location.y]
3655 node_tree = node.id_data
3656 props_to_copy = 'bl_idname name location height width'.split(' ')
3658 reconnections = []
3659 mappings = chain.from_iterable([node.inputs, node.outputs])
3660 for i in (i for i in mappings if i.is_linked):
3661 for L in i.links:
3662 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3664 props = {j: getattr(node, j) for j in props_to_copy}
3666 new_node = node_tree.nodes.new(props['bl_idname'])
3667 props_to_copy.pop(0)
3669 for prop in props_to_copy:
3670 setattr(new_node, prop, props[prop])
3672 nodes = node_tree.nodes
3673 nodes.remove(node)
3674 new_node.name = props['name']
3676 if parent:
3677 new_node.parent = parent
3678 new_node.location = node_loc
3680 for str_from, str_to in reconnections:
3681 node_tree.links.new(eval(str_from), eval(str_to))
3683 new_node.select = False
3684 success_names.append(new_node.name)
3686 # Reselect all nodes
3687 if selected_node_names and node_active_is_frame is False:
3688 for i in selected_node_names:
3689 node_tree.nodes[i].select = True
3691 if active_node_name is not None:
3692 node_tree.nodes[active_node_name].select = True
3693 node_tree.nodes.active = node_tree.nodes[active_node_name]
3695 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3696 return {'FINISHED'}
3700 # P A N E L
3703 def drawlayout(context, layout, mode='non-panel'):
3704 tree_type = context.space_data.tree_type
3706 col = layout.column(align=True)
3707 col.menu(NWMergeNodesMenu.bl_idname)
3708 col.separator()
3710 col = layout.column(align=True)
3711 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3712 col.separator()
3714 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3715 col = layout.column(align=True)
3716 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3717 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3718 col.separator()
3720 col = layout.column(align=True)
3721 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3722 col.operator(NWSwapLinks.bl_idname)
3723 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3724 col.separator()
3726 col = layout.column(align=True)
3727 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3728 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3729 col.separator()
3731 col = layout.column(align=True)
3732 if mode == 'panel':
3733 row = col.row(align=True)
3734 row.operator(NWClearLabel.bl_idname).option = True
3735 row.operator(NWModifyLabels.bl_idname)
3736 else:
3737 col.operator(NWClearLabel.bl_idname).option = True
3738 col.operator(NWModifyLabels.bl_idname)
3739 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3740 col.separator()
3741 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3742 col.separator()
3744 col = layout.column(align=True)
3745 if tree_type == 'CompositorNodeTree':
3746 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
3747 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
3748 col.separator()
3750 col = layout.column(align=True)
3751 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3752 col.separator()
3754 col = layout.column(align=True)
3755 col.operator(NWAlignNodes.bl_idname, icon='ALIGN')
3756 col.separator()
3758 col = layout.column(align=True)
3759 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3760 col.separator()
3763 class NodeWranglerPanel(Panel, NWBase):
3764 bl_idname = "NODE_PT_nw_node_wrangler"
3765 bl_space_type = 'NODE_EDITOR'
3766 bl_label = "Node Wrangler"
3767 bl_region_type = "TOOLS"
3768 bl_category = "Node Wrangler"
3770 prepend: StringProperty(
3771 name='prepend',
3773 append: StringProperty()
3774 remove: StringProperty()
3776 def draw(self, context):
3777 self.layout.label(text="(Quick access: Ctrl+Space)")
3778 drawlayout(context, self.layout, mode='panel')
3782 # M E N U S
3784 class NodeWranglerMenu(Menu, NWBase):
3785 bl_idname = "NODE_MT_nw_node_wrangler_menu"
3786 bl_label = "Node Wrangler"
3788 def draw(self, context):
3789 drawlayout(context, self.layout)
3792 class NWMergeNodesMenu(Menu, NWBase):
3793 bl_idname = "NODE_MT_nw_merge_nodes_menu"
3794 bl_label = "Merge Selected Nodes"
3796 def draw(self, context):
3797 type = context.space_data.tree_type
3798 layout = self.layout
3799 if type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3800 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
3801 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
3802 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3803 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
3804 props.mode = 'MIX'
3805 props.merge_type = 'ZCOMBINE'
3806 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3807 props.mode = 'MIX'
3808 props.merge_type = 'ALPHAOVER'
3811 class NWMergeShadersMenu(Menu, NWBase):
3812 bl_idname = "NODE_MT_nw_merge_shaders_menu"
3813 bl_label = "Merge Selected Nodes using Shaders"
3815 def draw(self, context):
3816 layout = self.layout
3817 for type in ('MIX', 'ADD'):
3818 props = layout.operator(NWMergeNodes.bl_idname, text=type)
3819 props.mode = type
3820 props.merge_type = 'SHADER'
3823 class NWMergeMixMenu(Menu, NWBase):
3824 bl_idname = "NODE_MT_nw_merge_mix_menu"
3825 bl_label = "Merge Selected Nodes using Mix"
3827 def draw(self, context):
3828 layout = self.layout
3829 for type, name, description in blend_types:
3830 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3831 props.mode = type
3832 props.merge_type = 'MIX'
3835 class NWConnectionListOutputs(Menu, NWBase):
3836 bl_idname = "NODE_MT_nw_connection_list_out"
3837 bl_label = "From:"
3839 def draw(self, context):
3840 layout = self.layout
3841 nodes, links = get_nodes_links(context)
3843 n1 = nodes[context.scene.NWLazySource]
3845 if n1.type == "R_LAYERS":
3846 index=0
3847 for o in n1.outputs:
3848 if o.enabled: # Check which passes the render layer has enabled
3849 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3850 index+=1
3851 else:
3852 index=0
3853 for o in n1.outputs:
3854 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3855 index+=1
3858 class NWConnectionListInputs(Menu, NWBase):
3859 bl_idname = "NODE_MT_nw_connection_list_in"
3860 bl_label = "To:"
3862 def draw(self, context):
3863 layout = self.layout
3864 nodes, links = get_nodes_links(context)
3866 n2 = nodes[context.scene.NWLazyTarget]
3868 index = 0
3869 for i in n2.inputs:
3870 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
3871 op.from_socket = context.scene.NWSourceSocket
3872 op.to_socket = index
3873 index+=1
3876 class NWMergeMathMenu(Menu, NWBase):
3877 bl_idname = "NODE_MT_nw_merge_math_menu"
3878 bl_label = "Merge Selected Nodes using Math"
3880 def draw(self, context):
3881 layout = self.layout
3882 for type, name, description in operations:
3883 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3884 props.mode = type
3885 props.merge_type = 'MATH'
3888 class NWBatchChangeNodesMenu(Menu, NWBase):
3889 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3890 bl_label = "Batch Change Selected Nodes"
3892 def draw(self, context):
3893 layout = self.layout
3894 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3895 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3898 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3899 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3900 bl_label = "Batch Change Blend Type"
3902 def draw(self, context):
3903 layout = self.layout
3904 for type, name, description in blend_types:
3905 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3906 props.blend_type = type
3907 props.operation = 'CURRENT'
3910 class NWBatchChangeOperationMenu(Menu, NWBase):
3911 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3912 bl_label = "Batch Change Math Operation"
3914 def draw(self, context):
3915 layout = self.layout
3916 for type, name, description in operations:
3917 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3918 props.blend_type = 'CURRENT'
3919 props.operation = type
3922 class NWCopyToSelectedMenu(Menu, NWBase):
3923 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
3924 bl_label = "Copy to Selected"
3926 def draw(self, context):
3927 layout = self.layout
3928 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
3929 layout.menu(NWCopyLabelMenu.bl_idname)
3932 class NWCopyLabelMenu(Menu, NWBase):
3933 bl_idname = "NODE_MT_nw_copy_label_menu"
3934 bl_label = "Copy Label"
3936 def draw(self, context):
3937 layout = self.layout
3938 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
3939 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
3940 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
3943 class NWAddReroutesMenu(Menu, NWBase):
3944 bl_idname = "NODE_MT_nw_add_reroutes_menu"
3945 bl_label = "Add Reroutes"
3946 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
3948 def draw(self, context):
3949 layout = self.layout
3950 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
3951 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
3952 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
3955 class NWLinkActiveToSelectedMenu(Menu, NWBase):
3956 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
3957 bl_label = "Link Active to Selected"
3959 def draw(self, context):
3960 layout = self.layout
3961 layout.menu(NWLinkStandardMenu.bl_idname)
3962 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
3963 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
3966 class NWLinkStandardMenu(Menu, NWBase):
3967 bl_idname = "NODE_MT_nw_link_standard_menu"
3968 bl_label = "To All Selected"
3970 def draw(self, context):
3971 layout = self.layout
3972 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3973 props.replace = False
3974 props.use_node_name = False
3975 props.use_outputs_names = False
3976 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3977 props.replace = True
3978 props.use_node_name = False
3979 props.use_outputs_names = False
3982 class NWLinkUseNodeNameMenu(Menu, NWBase):
3983 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
3984 bl_label = "Use Node Name/Label"
3986 def draw(self, context):
3987 layout = self.layout
3988 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3989 props.replace = False
3990 props.use_node_name = True
3991 props.use_outputs_names = False
3992 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3993 props.replace = True
3994 props.use_node_name = True
3995 props.use_outputs_names = False
3998 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
3999 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
4000 bl_label = "Use Outputs Names"
4002 def draw(self, context):
4003 layout = self.layout
4004 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4005 props.replace = False
4006 props.use_node_name = False
4007 props.use_outputs_names = True
4008 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4009 props.replace = True
4010 props.use_node_name = False
4011 props.use_outputs_names = True
4014 class NWVertColMenu(bpy.types.Menu):
4015 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
4016 bl_label = "Vertex Colors"
4018 @classmethod
4019 def poll(cls, context):
4020 valid = False
4021 if nw_check(context):
4022 snode = context.space_data
4023 valid = snode.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context)
4024 return valid
4026 def draw(self, context):
4027 l = self.layout
4028 nodes, links = get_nodes_links(context)
4029 mat = context.object.active_material
4031 objs = []
4032 for obj in bpy.data.objects:
4033 for slot in obj.material_slots:
4034 if slot.material == mat:
4035 objs.append(obj)
4036 vcols = []
4037 for obj in objs:
4038 if obj.data.vertex_colors:
4039 for vcol in obj.data.vertex_colors:
4040 vcols.append(vcol.name)
4041 vcols = list(set(vcols)) # get a unique list
4043 if vcols:
4044 for vcol in vcols:
4045 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
4046 else:
4047 l.label("No Vertex Color layers on objects with this material")
4050 class NWSwitchNodeTypeMenu(Menu, NWBase):
4051 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4052 bl_label = "Switch Type to..."
4054 def draw(self, context):
4055 layout = self.layout
4056 tree = context.space_data.node_tree
4057 if tree.type == 'SHADER':
4058 if is_cycles_or_eevee(context):
4059 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
4060 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
4061 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
4062 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
4063 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
4064 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
4065 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
4066 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
4067 else:
4068 layout.menu(NWSwitchMatInputSubmenu.bl_idname)
4069 layout.menu(NWSwitchMatOutputSubmenu.bl_idname)
4070 layout.menu(NWSwitchMatColorSubmenu.bl_idname)
4071 layout.menu(NWSwitchMatVectorSubmenu.bl_idname)
4072 layout.menu(NWSwitchMatConverterSubmenu.bl_idname)
4073 layout.menu(NWSwitchMatLayoutSubmenu.bl_idname)
4074 if tree.type == 'COMPOSITING':
4075 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
4076 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
4077 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
4078 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
4079 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
4080 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
4081 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
4082 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
4083 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
4084 if tree.type == 'TEXTURE':
4085 layout.menu(NWSwitchTexInputSubmenu.bl_idname)
4086 layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
4087 layout.menu(NWSwitchTexColorSubmenu.bl_idname)
4088 layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
4089 layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
4090 layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
4091 layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
4092 layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
4095 class NWSwitchShadersInputSubmenu(Menu, NWBase):
4096 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
4097 bl_label = "Input"
4099 def draw(self, context):
4100 layout = self.layout
4101 for ident, node_type, rna_name in sorted(shaders_input_nodes_props, key=lambda k: k[2]):
4102 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4103 props.to_type = ident
4106 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
4107 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
4108 bl_label = "Output"
4110 def draw(self, context):
4111 layout = self.layout
4112 for ident, node_type, rna_name in sorted(shaders_output_nodes_props, key=lambda k: k[2]):
4113 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4114 props.to_type = ident
4117 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
4118 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
4119 bl_label = "Shader"
4121 def draw(self, context):
4122 layout = self.layout
4123 for ident, node_type, rna_name in sorted(shaders_shader_nodes_props, key=lambda k: k[2]):
4124 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4125 props.to_type = ident
4128 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
4129 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
4130 bl_label = "Texture"
4132 def draw(self, context):
4133 layout = self.layout
4134 for ident, node_type, rna_name in sorted(shaders_texture_nodes_props, key=lambda k: k[2]):
4135 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4136 props.to_type = ident
4139 class NWSwitchShadersColorSubmenu(Menu, NWBase):
4140 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
4141 bl_label = "Color"
4143 def draw(self, context):
4144 layout = self.layout
4145 for ident, node_type, rna_name in sorted(shaders_color_nodes_props, key=lambda k: k[2]):
4146 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4147 props.to_type = ident
4150 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
4151 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
4152 bl_label = "Vector"
4154 def draw(self, context):
4155 layout = self.layout
4156 for ident, node_type, rna_name in sorted(shaders_vector_nodes_props, key=lambda k: k[2]):
4157 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4158 props.to_type = ident
4161 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
4162 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
4163 bl_label = "Converter"
4165 def draw(self, context):
4166 layout = self.layout
4167 for ident, node_type, rna_name in sorted(shaders_converter_nodes_props, key=lambda k: k[2]):
4168 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4169 props.to_type = ident
4172 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
4173 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
4174 bl_label = "Layout"
4176 def draw(self, context):
4177 layout = self.layout
4178 for ident, node_type, rna_name in sorted(shaders_layout_nodes_props, key=lambda k: k[2]):
4179 if node_type != 'FRAME':
4180 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4181 props.to_type = ident
4184 class NWSwitchCompoInputSubmenu(Menu, NWBase):
4185 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
4186 bl_label = "Input"
4188 def draw(self, context):
4189 layout = self.layout
4190 for ident, node_type, rna_name in sorted(compo_input_nodes_props, key=lambda k: k[2]):
4191 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4192 props.to_type = ident
4195 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
4196 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
4197 bl_label = "Output"
4199 def draw(self, context):
4200 layout = self.layout
4201 for ident, node_type, rna_name in sorted(compo_output_nodes_props, key=lambda k: k[2]):
4202 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4203 props.to_type = ident
4206 class NWSwitchCompoColorSubmenu(Menu, NWBase):
4207 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
4208 bl_label = "Color"
4210 def draw(self, context):
4211 layout = self.layout
4212 for ident, node_type, rna_name in sorted(compo_color_nodes_props, key=lambda k: k[2]):
4213 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4214 props.to_type = ident
4217 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
4218 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
4219 bl_label = "Converter"
4221 def draw(self, context):
4222 layout = self.layout
4223 for ident, node_type, rna_name in sorted(compo_converter_nodes_props, key=lambda k: k[2]):
4224 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4225 props.to_type = ident
4228 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
4229 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
4230 bl_label = "Filter"
4232 def draw(self, context):
4233 layout = self.layout
4234 for ident, node_type, rna_name in sorted(compo_filter_nodes_props, key=lambda k: k[2]):
4235 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4236 props.to_type = ident
4239 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
4240 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
4241 bl_label = "Vector"
4243 def draw(self, context):
4244 layout = self.layout
4245 for ident, node_type, rna_name in sorted(compo_vector_nodes_props, key=lambda k: k[2]):
4246 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4247 props.to_type = ident
4250 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
4251 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
4252 bl_label = "Matte"
4254 def draw(self, context):
4255 layout = self.layout
4256 for ident, node_type, rna_name in sorted(compo_matte_nodes_props, key=lambda k: k[2]):
4257 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4258 props.to_type = ident
4261 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
4262 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
4263 bl_label = "Distort"
4265 def draw(self, context):
4266 layout = self.layout
4267 for ident, node_type, rna_name in sorted(compo_distort_nodes_props, key=lambda k: k[2]):
4268 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4269 props.to_type = ident
4272 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
4273 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
4274 bl_label = "Layout"
4276 def draw(self, context):
4277 layout = self.layout
4278 for ident, node_type, rna_name in sorted(compo_layout_nodes_props, key=lambda k: k[2]):
4279 if node_type != 'FRAME':
4280 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4281 props.to_type = ident
4284 class NWSwitchMatInputSubmenu(Menu, NWBase):
4285 bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
4286 bl_label = "Input"
4288 def draw(self, context):
4289 layout = self.layout
4290 for ident, node_type, rna_name in sorted(blender_mat_input_nodes_props, key=lambda k: k[2]):
4291 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4292 props.to_type = ident
4295 class NWSwitchMatOutputSubmenu(Menu, NWBase):
4296 bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
4297 bl_label = "Output"
4299 def draw(self, context):
4300 layout = self.layout
4301 for ident, node_type, rna_name in sorted(blender_mat_output_nodes_props, key=lambda k: k[2]):
4302 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4303 props.to_type = ident
4306 class NWSwitchMatColorSubmenu(Menu, NWBase):
4307 bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
4308 bl_label = "Color"
4310 def draw(self, context):
4311 layout = self.layout
4312 for ident, node_type, rna_name in sorted(blender_mat_color_nodes_props, key=lambda k: k[2]):
4313 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4314 props.to_type = ident
4317 class NWSwitchMatVectorSubmenu(Menu, NWBase):
4318 bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
4319 bl_label = "Vector"
4321 def draw(self, context):
4322 layout = self.layout
4323 for ident, node_type, rna_name in sorted(blender_mat_vector_nodes_props, key=lambda k: k[2]):
4324 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4325 props.to_type = ident
4328 class NWSwitchMatConverterSubmenu(Menu, NWBase):
4329 bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
4330 bl_label = "Converter"
4332 def draw(self, context):
4333 layout = self.layout
4334 for ident, node_type, rna_name in sorted(blender_mat_converter_nodes_props, key=lambda k: k[2]):
4335 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4336 props.to_type = ident
4339 class NWSwitchMatLayoutSubmenu(Menu, NWBase):
4340 bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
4341 bl_label = "Layout"
4343 def draw(self, context):
4344 layout = self.layout
4345 for ident, node_type, rna_name in sorted(blender_mat_layout_nodes_props, key=lambda k: k[2]):
4346 if node_type != 'FRAME':
4347 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4348 props.to_type = ident
4351 class NWSwitchTexInputSubmenu(Menu, NWBase):
4352 bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
4353 bl_label = "Input"
4355 def draw(self, context):
4356 layout = self.layout
4357 for ident, node_type, rna_name in sorted(texture_input_nodes_props, key=lambda k: k[2]):
4358 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4359 props.to_type = ident
4362 class NWSwitchTexOutputSubmenu(Menu, NWBase):
4363 bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
4364 bl_label = "Output"
4366 def draw(self, context):
4367 layout = self.layout
4368 for ident, node_type, rna_name in sorted(texture_output_nodes_props, key=lambda k: k[2]):
4369 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4370 props.to_type = ident
4373 class NWSwitchTexColorSubmenu(Menu, NWBase):
4374 bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
4375 bl_label = "Color"
4377 def draw(self, context):
4378 layout = self.layout
4379 for ident, node_type, rna_name in sorted(texture_color_nodes_props, key=lambda k: k[2]):
4380 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4381 props.to_type = ident
4384 class NWSwitchTexPatternSubmenu(Menu, NWBase):
4385 bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
4386 bl_label = "Pattern"
4388 def draw(self, context):
4389 layout = self.layout
4390 for ident, node_type, rna_name in sorted(texture_pattern_nodes_props, key=lambda k: k[2]):
4391 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4392 props.to_type = ident
4395 class NWSwitchTexTexturesSubmenu(Menu, NWBase):
4396 bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
4397 bl_label = "Textures"
4399 def draw(self, context):
4400 layout = self.layout
4401 for ident, node_type, rna_name in sorted(texture_textures_nodes_props, key=lambda k: k[2]):
4402 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4403 props.to_type = ident
4406 class NWSwitchTexConverterSubmenu(Menu, NWBase):
4407 bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
4408 bl_label = "Converter"
4410 def draw(self, context):
4411 layout = self.layout
4412 for ident, node_type, rna_name in sorted(texture_converter_nodes_props, key=lambda k: k[2]):
4413 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4414 props.to_type = ident
4417 class NWSwitchTexDistortSubmenu(Menu, NWBase):
4418 bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
4419 bl_label = "Distort"
4421 def draw(self, context):
4422 layout = self.layout
4423 for ident, node_type, rna_name in sorted(texture_distort_nodes_props, key=lambda k: k[2]):
4424 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4425 props.to_type = ident
4428 class NWSwitchTexLayoutSubmenu(Menu, NWBase):
4429 bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
4430 bl_label = "Layout"
4432 def draw(self, context):
4433 layout = self.layout
4434 for ident, node_type, rna_name in sorted(texture_layout_nodes_props, key=lambda k: k[2]):
4435 if node_type != 'FRAME':
4436 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4437 props.to_type = ident
4441 # APPENDAGES TO EXISTING UI
4445 def select_parent_children_buttons(self, context):
4446 layout = self.layout
4447 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4448 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4451 def attr_nodes_menu_func(self, context):
4452 col = self.layout.column(align=True)
4453 col.menu("NODE_MT_nw_node_vertex_color_menu")
4454 col.separator()
4457 def multipleimages_menu_func(self, context):
4458 col = self.layout.column(align=True)
4459 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4460 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4461 col.separator()
4464 def bgreset_menu_func(self, context):
4465 self.layout.operator(NWResetBG.bl_idname)
4468 def save_viewer_menu_func(self, context):
4469 if nw_check(context):
4470 if context.space_data.tree_type == 'CompositorNodeTree':
4471 if context.scene.node_tree.nodes.active:
4472 if context.scene.node_tree.nodes.active.type == "VIEWER":
4473 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4476 def reset_nodes_button(self, context):
4477 node_active = context.active_node
4478 node_selected = context.selected_nodes
4479 node_ignore = ["FRAME","REROUTE", "GROUP"]
4481 # Check if active node is in the selection and respective type
4482 if (len(node_selected) == 1) and node_active.select and node_active.type not in node_ignore:
4483 row = self.layout.row()
4484 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4485 self.layout.separator()
4487 elif (len(node_selected) == 1) and node_active.select and node_active.type == "FRAME":
4488 row = self.layout.row()
4489 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4490 self.layout.separator()
4494 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4497 addon_keymaps = []
4498 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4499 # props entry: (property name, property value)
4500 kmi_defs = (
4501 # MERGE NODES
4502 # NWMergeNodes with Ctrl (AUTO).
4503 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4504 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4505 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4506 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4507 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4508 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4509 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4510 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4511 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4512 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4513 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4514 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4515 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4516 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4517 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4518 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4519 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4520 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4521 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4522 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4523 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4524 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4525 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4526 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4527 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4528 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4529 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4530 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4531 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4532 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4533 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4534 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4535 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4536 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4537 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4538 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4539 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4540 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4541 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4542 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4543 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4544 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4545 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4546 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4547 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4548 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4549 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4550 # NWMergeNodes with Ctrl Shift (MATH)
4551 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4552 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4553 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4554 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4555 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4556 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4557 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4558 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4559 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4560 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4561 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4562 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4563 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4564 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4565 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4566 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4567 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4568 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4569 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4570 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4571 # BATCH CHANGE NODES
4572 # NWBatchChangeNodes with Alt
4573 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4574 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4575 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4576 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4577 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4578 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4579 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4580 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4581 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4582 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4583 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4584 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4585 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4586 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4587 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4588 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4589 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4590 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4591 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4592 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4593 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4594 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4595 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4596 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4597 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4598 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4599 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4600 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4601 # LINK ACTIVE TO SELECTED
4602 # Don't use names, don't replace links (K)
4603 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4604 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4605 # Don't use names, replace links (Shift K)
4606 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4607 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4608 # Use node name, don't replace links (')
4609 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4610 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4611 # Use node name, replace links (Shift ')
4612 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4613 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4614 # Don't use names, don't replace links (;)
4615 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4616 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4617 # Don't use names, replace links (')
4618 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4619 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4620 # CHANGE MIX FACTOR
4621 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4622 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4623 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4624 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4625 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4626 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4627 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4628 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4629 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4630 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4631 # CLEAR LABEL (Alt L)
4632 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4633 # MODIFY LABEL (Alt Shift L)
4634 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4635 # Copy Label from active to selected
4636 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4637 # DETACH OUTPUTS (Alt Shift D)
4638 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4639 # LINK TO OUTPUT NODE (O)
4640 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4641 # SELECT PARENT/CHILDREN
4642 # Select Children
4643 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4644 # Select Parent
4645 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4646 # Add Texture Setup
4647 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4648 # Add Principled BSDF Texture Setup
4649 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4650 # Reset backdrop
4651 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4652 # Delete unused
4653 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4654 # Frame Seleted
4655 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4656 # Swap Outputs
4657 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4658 # Emission Viewer
4659 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4660 # Reload Images
4661 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4662 # Lazy Mix
4663 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, None, "Lazy Mix"),
4664 # Lazy Connect
4665 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, False, False, None, "Lazy Connect"),
4666 # Lazy Connect with Menu
4667 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4668 # Viewer Tile Center
4669 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4670 # Align Nodes
4671 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4672 # Reset Nodes (Back Space)
4673 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4674 # MENUS
4675 ('wm.call_menu', 'SPACE', 'PRESS', True, False, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wranger menu"),
4676 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4677 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4678 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4679 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4680 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4684 classes = (
4685 NWPrincipledPreferences,
4686 NWNodeWrangler,
4687 NWLazyMix,
4688 NWLazyConnect,
4689 NWDeleteUnused,
4690 NWSwapLinks,
4691 NWResetBG,
4692 NWAddAttrNode,
4693 NWEmissionViewer,
4694 NWFrameSelected,
4695 NWReloadImages,
4696 NWSwitchNodeType,
4697 NWMergeNodes,
4698 NWBatchChangeNodes,
4699 NWChangeMixFactor,
4700 NWCopySettings,
4701 NWCopyLabel,
4702 NWClearLabel,
4703 NWModifyLabels,
4704 NWAddTextureSetup,
4705 NWAddPrincipledSetup,
4706 NWAddReroutes,
4707 NWLinkActiveToSelected,
4708 NWAlignNodes,
4709 NWSelectParentChildren,
4710 NWDetachOutputs,
4711 NWLinkToOutputNode,
4712 NWMakeLink,
4713 NWCallInputsMenu,
4714 NWAddSequence,
4715 NWAddMultipleImages,
4716 NWViewerFocus,
4717 NWSaveViewer,
4718 NWResetNodes,
4719 NodeWranglerPanel,
4720 NodeWranglerMenu,
4721 NWMergeNodesMenu,
4722 NWMergeShadersMenu,
4723 NWMergeMixMenu,
4724 NWConnectionListOutputs,
4725 NWConnectionListInputs,
4726 NWMergeMathMenu,
4727 NWBatchChangeNodesMenu,
4728 NWBatchChangeBlendTypeMenu,
4729 NWBatchChangeOperationMenu,
4730 NWCopyToSelectedMenu,
4731 NWCopyLabelMenu,
4732 NWAddReroutesMenu,
4733 NWLinkActiveToSelectedMenu,
4734 NWLinkStandardMenu,
4735 NWLinkUseNodeNameMenu,
4736 NWLinkUseOutputsNamesMenu,
4737 NWVertColMenu,
4738 NWSwitchNodeTypeMenu,
4739 NWSwitchShadersInputSubmenu,
4740 NWSwitchShadersOutputSubmenu,
4741 NWSwitchShadersShaderSubmenu,
4742 NWSwitchShadersTextureSubmenu,
4743 NWSwitchShadersColorSubmenu,
4744 NWSwitchShadersVectorSubmenu,
4745 NWSwitchShadersConverterSubmenu,
4746 NWSwitchShadersLayoutSubmenu,
4747 NWSwitchCompoInputSubmenu,
4748 NWSwitchCompoOutputSubmenu,
4749 NWSwitchCompoColorSubmenu,
4750 NWSwitchCompoConverterSubmenu,
4751 NWSwitchCompoFilterSubmenu,
4752 NWSwitchCompoVectorSubmenu,
4753 NWSwitchCompoMatteSubmenu,
4754 NWSwitchCompoDistortSubmenu,
4755 NWSwitchCompoLayoutSubmenu,
4756 NWSwitchMatInputSubmenu,
4757 NWSwitchMatOutputSubmenu,
4758 NWSwitchMatColorSubmenu,
4759 NWSwitchMatVectorSubmenu,
4760 NWSwitchMatConverterSubmenu,
4761 NWSwitchMatLayoutSubmenu,
4762 NWSwitchTexInputSubmenu,
4763 NWSwitchTexOutputSubmenu,
4764 NWSwitchTexColorSubmenu,
4765 NWSwitchTexPatternSubmenu,
4766 NWSwitchTexTexturesSubmenu,
4767 NWSwitchTexConverterSubmenu,
4768 NWSwitchTexDistortSubmenu,
4769 NWSwitchTexLayoutSubmenu,
4772 def register():
4773 from bpy.utils import register_class
4775 # props
4776 bpy.types.Scene.NWBusyDrawing = StringProperty(
4777 name="Busy Drawing!",
4778 default="",
4779 description="An internal property used to store only the first mouse position")
4780 bpy.types.Scene.NWLazySource = StringProperty(
4781 name="Lazy Source!",
4782 default="x",
4783 description="An internal property used to store the first node in a Lazy Connect operation")
4784 bpy.types.Scene.NWLazyTarget = StringProperty(
4785 name="Lazy Target!",
4786 default="x",
4787 description="An internal property used to store the last node in a Lazy Connect operation")
4788 bpy.types.Scene.NWSourceSocket = IntProperty(
4789 name="Source Socket!",
4790 default=0,
4791 description="An internal property used to store the source socket in a Lazy Connect operation")
4793 for cls in classes:
4794 register_class(cls)
4796 # keymaps
4797 addon_keymaps.clear()
4798 kc = bpy.context.window_manager.keyconfigs.addon
4799 if kc:
4800 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
4801 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
4802 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
4803 if props:
4804 for prop, value in props:
4805 setattr(kmi.properties, prop, value)
4806 addon_keymaps.append((km, kmi))
4808 # menu items
4809 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
4810 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4811 bpy.types.NODE_PT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4812 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
4813 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
4814 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4815 bpy.types.NODE_PT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4816 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4817 bpy.types.NODE_PT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4818 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
4819 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
4822 def unregister():
4823 from bpy.utils import unregister_class
4825 # props
4826 del bpy.types.Scene.NWBusyDrawing
4827 del bpy.types.Scene.NWLazySource
4828 del bpy.types.Scene.NWLazyTarget
4829 del bpy.types.Scene.NWSourceSocket
4831 # keymaps
4832 for km, kmi in addon_keymaps:
4833 km.keymap_items.remove(kmi)
4834 addon_keymaps.clear()
4836 # menuitems
4837 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
4838 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4839 bpy.types.NODE_PT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4840 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
4841 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
4842 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4843 bpy.types.NODE_PT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4844 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
4845 bpy.types.NODE_PT_category_CMP_INPUT.remove(multipleimages_menu_func)
4846 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
4847 bpy.types.NODE_MT_node.remove(reset_nodes_button)
4849 for cls in classes:
4850 unregister_class(cls)
4852 if __name__ == "__main__":
4853 register()