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 #####
20 "name": "Node Wrangler",
21 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
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",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Nodes/Nodes_Efficiency_Tools",
34 from bpy
.types
import Operator
, Panel
, Menu
35 from bpy
.props
import (
44 from bpy_extras
.io_utils
import ImportHelper
, ExportHelper
45 from gpu_extras
.batch
import batch_for_shader
46 from mathutils
import Vector
47 from math
import cos
, sin
, pi
, hypot
51 from itertools
import chain
53 from collections
import namedtuple
57 # list of outputs of Input Render Layer
58 # with attributes determinig if pass is used,
59 # and MultiLayer EXR outputs names and corresponding render engines
61 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
62 RL_entry
= namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
64 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
65 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
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', False, True),
70 RL_entry('use_pass_environment', 'Environment', 'Env', False, 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', False, False),
75 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
76 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
77 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
78 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
79 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
80 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
81 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
82 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
83 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
84 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
85 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
86 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
87 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
88 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
92 # (rna_type.identifier, type, rna_type.name)
93 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
94 shaders_input_nodes_props
= (
95 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
96 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
97 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
98 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
99 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
100 ('ShaderNodeRGB', 'RGB', 'RGB'),
101 ('ShaderNodeValue', 'VALUE', 'Value'),
102 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
103 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
104 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
105 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
106 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
107 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
108 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
109 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
111 # (rna_type.identifier, type, rna_type.name)
112 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
113 shaders_output_nodes_props
= (
114 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
115 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
116 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
118 # (rna_type.identifier, type, rna_type.name)
119 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
120 shaders_shader_nodes_props
= (
121 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
122 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
123 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
124 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
125 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
126 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
127 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
128 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
129 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
130 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
131 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
132 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
133 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
134 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
135 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
136 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
137 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
138 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
139 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
140 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
142 # (rna_type.identifier, type, rna_type.name)
143 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
144 shaders_texture_nodes_props
= (
145 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
146 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
147 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
148 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
149 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
150 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
151 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
152 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
153 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
154 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
155 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
156 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
158 # (rna_type.identifier, type, rna_type.name)
159 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
160 shaders_color_nodes_props
= (
161 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
162 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
163 ('ShaderNodeInvert', 'INVERT', 'Invert'),
164 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
165 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
166 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
167 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
169 # (rna_type.identifier, type, rna_type.name)
170 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
171 shaders_vector_nodes_props
= (
172 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
173 ('ShaderNodeBump', 'BUMP', 'Bump'),
174 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
175 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
176 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
177 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
179 # (rna_type.identifier, type, rna_type.name)
180 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
181 shaders_converter_nodes_props
= (
182 ('ShaderNodeMath', 'MATH', 'Math'),
183 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
184 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
185 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
186 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
187 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
188 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
189 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
190 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
191 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
192 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
193 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
195 # (rna_type.identifier, type, rna_type.name)
196 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
197 shaders_layout_nodes_props
= (
198 ('NodeFrame', 'FRAME', 'Frame'),
199 ('NodeReroute', 'REROUTE', 'Reroute'),
203 # (rna_type.identifier, type, rna_type.name)
204 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
205 compo_input_nodes_props
= (
206 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
207 ('CompositorNodeImage', 'IMAGE', 'Image'),
208 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
209 ('CompositorNodeMask', 'MASK', 'Mask'),
210 ('CompositorNodeRGB', 'RGB', 'RGB'),
211 ('CompositorNodeValue', 'VALUE', 'Value'),
212 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
213 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
214 ('CompositorNodeTime', 'TIME', 'Time'),
215 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
217 # (rna_type.identifier, type, rna_type.name)
218 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
219 compo_output_nodes_props
= (
220 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
221 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
222 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
223 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
224 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
226 # (rna_type.identifier, type, rna_type.name)
227 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
228 compo_color_nodes_props
= (
229 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
230 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
231 ('CompositorNodeInvert', 'INVERT', 'Invert'),
232 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
233 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
234 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
235 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
236 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
237 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
238 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
239 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
240 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
242 # (rna_type.identifier, type, rna_type.name)
243 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
244 compo_converter_nodes_props
= (
245 ('CompositorNodeMath', 'MATH', 'Math'),
246 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
247 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
248 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
249 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
250 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
251 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
252 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
253 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
254 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
255 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
256 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
257 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
258 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
260 # (rna_type.identifier, type, rna_type.name)
261 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
262 compo_filter_nodes_props
= (
263 ('CompositorNodeBlur', 'BLUR', 'Blur'),
264 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
265 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
266 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
267 ('CompositorNodeFilter', 'FILTER', 'Filter'),
268 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
269 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
270 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
271 ('CompositorNodeGlare', 'GLARE', 'Glare'),
272 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
273 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
274 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
275 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
277 # (rna_type.identifier, type, rna_type.name)
278 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
279 compo_vector_nodes_props
= (
280 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
281 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
282 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
283 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
284 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
286 # (rna_type.identifier, type, rna_type.name)
287 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
288 compo_matte_nodes_props
= (
289 ('CompositorNodeKeying', 'KEYING', 'Keying'),
290 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
291 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
292 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
293 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
294 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
295 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
296 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
297 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
298 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
299 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
300 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
302 # (rna_type.identifier, type, rna_type.name)
303 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
304 compo_distort_nodes_props
= (
305 ('CompositorNodeScale', 'SCALE', 'Scale'),
306 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
307 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
308 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
309 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
310 ('CompositorNodeFlip', 'FLIP', 'Flip'),
311 ('CompositorNodeCrop', 'CROP', 'Crop'),
312 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
313 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
314 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
315 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
316 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
317 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
319 # (rna_type.identifier, type, rna_type.name)
320 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
321 compo_layout_nodes_props
= (
322 ('NodeFrame', 'FRAME', 'Frame'),
323 ('NodeReroute', 'REROUTE', 'Reroute'),
324 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
326 # Blender Render material nodes
327 # (rna_type.identifier, type, rna_type.name)
328 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
329 blender_mat_input_nodes_props
= (
330 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
331 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
332 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
333 ('ShaderNodeValue', 'VALUE', 'Value'),
334 ('ShaderNodeRGB', 'RGB', 'RGB'),
335 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
336 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
337 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
340 # (rna_type.identifier, type, rna_type.name)
341 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
342 blender_mat_output_nodes_props
= (
343 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
346 # (rna_type.identifier, type, rna_type.name)
347 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
348 blender_mat_color_nodes_props
= (
349 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
350 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
351 ('ShaderNodeInvert', 'INVERT', 'Invert'),
352 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
355 # (rna_type.identifier, type, rna_type.name)
356 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
357 blender_mat_vector_nodes_props
= (
358 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
359 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
360 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
363 # (rna_type.identifier, type, rna_type.name)
364 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
365 blender_mat_converter_nodes_props
= (
366 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
367 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
368 ('ShaderNodeMath', 'MATH', 'Math'),
369 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
370 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
371 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
372 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
373 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
374 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
377 # (rna_type.identifier, type, rna_type.name)
378 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
379 blender_mat_layout_nodes_props
= (
380 ('NodeReroute', 'REROUTE', 'Reroute'),
384 # (rna_type.identifier, type, rna_type.name)
385 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
386 texture_input_nodes_props
= (
387 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
388 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
389 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
390 ('TextureNodeImage', 'IMAGE', 'Image'),
393 # (rna_type.identifier, type, rna_type.name)
394 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
395 texture_output_nodes_props
= (
396 ('TextureNodeOutput', 'OUTPUT', 'Output'),
397 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
400 # (rna_type.identifier, type, rna_type.name)
401 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
402 texture_color_nodes_props
= (
403 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
404 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
405 ('TextureNodeInvert', 'INVERT', 'Invert'),
406 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
407 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
408 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
411 # (rna_type.identifier, type, rna_type.name)
412 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
413 texture_pattern_nodes_props
= (
414 ('TextureNodeChecker', 'CHECKER', 'Checker'),
415 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
418 # (rna_type.identifier, type, rna_type.name)
419 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
420 texture_textures_nodes_props
= (
421 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
422 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
423 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
424 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
425 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
426 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
427 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
428 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
429 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
430 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
433 # (rna_type.identifier, type, rna_type.name)
434 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
435 texture_converter_nodes_props
= (
436 ('TextureNodeMath', 'MATH', 'Math'),
437 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
438 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
439 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
440 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
443 # (rna_type.identifier, type, rna_type.name)
444 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
445 texture_distort_nodes_props
= (
446 ('TextureNodeScale', 'SCALE', 'Scale'),
447 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
448 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
449 ('TextureNodeAt', 'AT', 'At'),
452 # (rna_type.identifier, type, rna_type.name)
453 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
454 texture_layout_nodes_props
= (
455 ('NodeReroute', 'REROUTE', 'Reroute'),
458 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
459 # used list, not tuple for easy merging with other lists.
461 ('MIX', 'Mix', 'Mix Mode'),
462 ('ADD', 'Add', 'Add Mode'),
463 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
464 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
465 ('SCREEN', 'Screen', 'Screen Mode'),
466 ('DIVIDE', 'Divide', 'Divide Mode'),
467 ('DIFFERENCE', 'Difference', 'Difference Mode'),
468 ('DARKEN', 'Darken', 'Darken Mode'),
469 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
470 ('OVERLAY', 'Overlay', 'Overlay Mode'),
471 ('DODGE', 'Dodge', 'Dodge Mode'),
472 ('BURN', 'Burn', 'Burn Mode'),
473 ('HUE', 'Hue', 'Hue Mode'),
474 ('SATURATION', 'Saturation', 'Saturation Mode'),
475 ('VALUE', 'Value', 'Value Mode'),
476 ('COLOR', 'Color', 'Color Mode'),
477 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
478 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
481 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
482 # used list, not tuple for easy merging with other lists.
484 ('ADD', 'Add', 'Add Mode'),
485 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
486 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
487 ('DIVIDE', 'Divide', 'Divide Mode'),
488 ('SINE', 'Sine', 'Sine Mode'),
489 ('COSINE', 'Cosine', 'Cosine Mode'),
490 ('TANGENT', 'Tangent', 'Tangent Mode'),
491 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
492 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
493 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
494 ('POWER', 'Power', 'Power Mode'),
495 ('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
496 ('MINIMUM', 'Minimum', 'Minimum Mode'),
497 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
498 ('ROUND', 'Round', 'Round Mode'),
499 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
500 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
501 ('MODULO', 'Modulo', 'Modulo Mode'),
502 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
505 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
506 # used list, not tuple for easy merging with other lists.
508 ('CURRENT', 'Current', 'Leave at current state'),
509 ('NEXT', 'Next', 'Next blend type/operation'),
510 ('PREV', 'Prev', 'Previous blend type/operation'),
515 (1.0, 1.0, 1.0, 0.7),
516 (1.0, 0.0, 0.0, 0.7),
520 (0.0, 0.0, 0.0, 1.0),
521 (0.38, 0.77, 0.38, 1.0),
522 (0.38, 0.77, 0.38, 1.0)
525 (0.0, 0.0, 0.0, 1.0),
526 (0.77, 0.77, 0.16, 1.0),
527 (0.77, 0.77, 0.16, 1.0)
530 (0.0, 0.0, 0.0, 1.0),
531 (0.38, 0.38, 0.77, 1.0),
532 (0.38, 0.38, 0.77, 1.0)
535 (0.0, 0.0, 0.0, 1.0),
536 (0.63, 0.63, 0.63, 1.0),
537 (0.63, 0.63, 0.63, 1.0)
540 (1.0, 1.0, 1.0, 0.7),
541 (0.0, 0.0, 0.0, 0.7),
547 def is_cycles_or_eevee(context
):
548 return context
.scene
.render
.engine
in {'CYCLES', 'BLENDER_EEVEE'}
551 def nice_hotkey_name(punc
):
552 # convert the ugly string name into the actual character
554 ('LEFTMOUSE', "LMB"),
555 ('MIDDLEMOUSE', "MMB"),
556 ('RIGHTMOUSE', "RMB"),
557 ('WHEELUPMOUSE', "Wheel Up"),
558 ('WHEELDOWNMOUSE', "Wheel Down"),
559 ('WHEELINMOUSE', "Wheel In"),
560 ('WHEELOUTMOUSE', "Wheel Out"),
573 ('LINE_FEED', "Enter"),
580 ('BACK_SLASH', "\\"),
582 ('NUMPAD_1', "Numpad 1"),
583 ('NUMPAD_2', "Numpad 2"),
584 ('NUMPAD_3', "Numpad 3"),
585 ('NUMPAD_4', "Numpad 4"),
586 ('NUMPAD_5', "Numpad 5"),
587 ('NUMPAD_6', "Numpad 6"),
588 ('NUMPAD_7', "Numpad 7"),
589 ('NUMPAD_8', "Numpad 8"),
590 ('NUMPAD_9', "Numpad 9"),
591 ('NUMPAD_0', "Numpad 0"),
592 ('NUMPAD_PERIOD', "Numpad ."),
593 ('NUMPAD_SLASH', "Numpad /"),
594 ('NUMPAD_ASTERIX', "Numpad *"),
595 ('NUMPAD_MINUS', "Numpad -"),
596 ('NUMPAD_ENTER', "Numpad Enter"),
597 ('NUMPAD_PLUS', "Numpad +"),
600 for (ugly
, nice
) in pairs
:
605 nice_punc
= punc
.replace("_", " ").title()
609 def force_update(context
):
610 context
.space_data
.node_tree
.update_tag()
614 prefs
= bpy
.context
.preferences
.system
615 return prefs
.dpi
* prefs
.pixel_size
/ 72
618 def node_mid_pt(node
, axis
):
620 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
622 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
628 def autolink(node1
, node2
, links
):
631 for outp
in node1
.outputs
:
632 for inp
in node2
.inputs
:
633 if not inp
.is_linked
and inp
.name
== outp
.name
:
638 for outp
in node1
.outputs
:
639 for inp
in node2
.inputs
:
640 if not inp
.is_linked
and inp
.type == outp
.type:
645 # force some connection even if the type doesn't match
646 for outp
in node1
.outputs
:
647 for inp
in node2
.inputs
:
648 if not inp
.is_linked
:
653 # even if no sockets are open, force one of matching type
654 for outp
in node1
.outputs
:
655 for inp
in node2
.inputs
:
656 if inp
.type == outp
.type:
662 for outp
in node1
.outputs
:
663 for inp
in node2
.inputs
:
668 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
672 def node_at_pos(nodes
, context
, event
):
673 nodes_near_mouse
= []
674 nodes_under_mouse
= []
677 store_mouse_cursor(context
, event
)
678 x
, y
= context
.space_data
.cursor_location
682 # Make a list of each corner (and middle of border) for each node.
683 # Will be sorted to find nearest point and thus nearest node
684 node_points_with_dist
= []
687 if node
.type != 'FRAME': # no point trying to link to a frame node
688 locx
= node
.location
.x
689 locy
= node
.location
.y
690 dimx
= node
.dimensions
.x
/dpifac()
691 dimy
= node
.dimensions
.y
/dpifac()
693 locx
+= node
.parent
.location
.x
694 locy
+= node
.parent
.location
.y
695 if node
.parent
.parent
:
696 locx
+= node
.parent
.parent
.location
.x
697 locy
+= node
.parent
.parent
.location
.y
698 if node
.parent
.parent
.parent
:
699 locx
+= node
.parent
.parent
.parent
.location
.x
700 locy
+= node
.parent
.parent
.parent
.location
.y
701 if node
.parent
.parent
.parent
.parent
:
702 # Support three levels or parenting
703 # There's got to be a better way to do this...
706 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
707 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
708 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
709 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
711 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
712 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
713 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
714 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
716 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
719 if node
.type != 'FRAME' and skipnode
== False:
720 locx
= node
.location
.x
721 locy
= node
.location
.y
722 dimx
= node
.dimensions
.x
/dpifac()
723 dimy
= node
.dimensions
.y
/dpifac()
725 locx
+= node
.parent
.location
.x
726 locy
+= node
.parent
.location
.y
727 if (locx
<= x
<= locx
+ dimx
) and \
728 (locy
- dimy
<= y
<= locy
):
729 nodes_under_mouse
.append(node
)
731 if len(nodes_under_mouse
) == 1:
732 if nodes_under_mouse
[0] != nearest_node
:
733 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
735 target_node
= nearest_node
# else use the nearest node
737 target_node
= nearest_node
741 def store_mouse_cursor(context
, event
):
742 space
= context
.space_data
743 v2d
= context
.region
.view2d
744 tree
= space
.edit_tree
746 # convert mouse position to the View2D for later node placement
747 if context
.region
.type == 'WINDOW':
748 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
750 space
.cursor_location
= tree
.view_center
752 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
753 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
755 vertices
= ((x1
, y1
), (x2
, y2
))
756 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
757 colour
[1]+(1.0-colour
[1])/4,
758 colour
[2]+(1.0-colour
[2])/4,
759 colour
[3]+(1.0-colour
[3])/4),
762 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
763 bgl
.glLineWidth(size
* dpifac())
769 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
770 radius
= radius
* dpifac()
772 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
773 radius
* sin(i
* 2 * pi
/ sides
) + my
)
774 for i
in range(sides
+ 1)]
776 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
778 shader
.uniform_float("color", colour
)
781 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
782 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
783 bottom_bar
= (16*dpifac()) + 1
785 radius
= radius
*dpifac()
787 nlocx
= (node
.location
.x
+1)*dpifac()
788 nlocy
= (node
.location
.y
+1)*dpifac()
789 ndimx
= node
.dimensions
.x
790 ndimy
= node
.dimensions
.y
791 # This is a stupid way to do this... TODO use while loop
793 nlocx
+= node
.parent
.location
.x
794 nlocy
+= node
.parent
.location
.y
795 if node
.parent
.parent
:
796 nlocx
+= node
.parent
.parent
.location
.x
797 nlocy
+= node
.parent
.parent
.location
.y
798 if node
.parent
.parent
.parent
:
799 nlocx
+= node
.parent
.parent
.parent
.location
.x
800 nlocy
+= node
.parent
.parent
.parent
.location
.y
805 if node
.type == 'REROUTE':
813 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
815 for i
in range(sides
+1):
817 if my
> bottom_bar
and mx
< area_width
:
818 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
819 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
820 vertices
.append((cosine
,sine
))
821 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
823 shader
.uniform_float("color", colour
)
827 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
829 for i
in range(sides
+1):
831 if my
> bottom_bar
and mx
< area_width
:
832 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
833 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
834 vertices
.append((cosine
,sine
))
835 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
837 shader
.uniform_float("color", colour
)
841 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
843 for i
in range(sides
+1):
845 if my
> bottom_bar
and mx
< area_width
:
846 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
847 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
848 vertices
.append((cosine
,sine
))
849 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
851 shader
.uniform_float("color", colour
)
854 # Bottom right corner
855 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
857 for i
in range(sides
+1):
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 vertices
.append((cosine
,sine
))
863 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
865 shader
.uniform_float("color", colour
)
868 # prepare drawing all edges in one batch
874 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
875 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
876 if m1x
< area_width
and m2x
< area_width
:
877 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
878 (m1x
,m1y
), (m1x
-radius
,m1y
)])
879 indices
.extend([(id_last
, id_last
+1, id_last
+3),
880 (id_last
+3, id_last
+1, id_last
+2)])
884 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
885 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
886 m1x
= min(m1x
, area_width
)
887 m2x
= min(m2x
, area_width
)
888 if m1y
> bottom_bar
and m2y
> bottom_bar
:
889 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
890 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
891 indices
.extend([(id_last
, id_last
+1, id_last
+3),
892 (id_last
+3, id_last
+1, id_last
+2)])
896 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
897 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
898 m1y
= max(m1y
, bottom_bar
)
899 m2y
= max(m2y
, bottom_bar
)
900 if m1x
< area_width
and m2x
< area_width
:
901 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
902 (m1x
+radius
,m1y
), (m1x
,m1y
)])
903 indices
.extend([(id_last
, id_last
+1, id_last
+3),
904 (id_last
+3, id_last
+1, id_last
+2)])
908 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
909 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
910 m1x
= min(m1x
, area_width
)
911 m2x
= min(m2x
, area_width
)
912 if m1y
> bottom_bar
and m2y
> bottom_bar
:
913 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
914 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
915 indices
.extend([(id_last
, id_last
+1, id_last
+3),
916 (id_last
+3, id_last
+1, id_last
+2)])
918 # now draw all edges in one batch
919 if len(vertices
) != 0:
920 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
922 shader
.uniform_float("color", colour
)
925 def draw_callback_nodeoutline(self
, context
, mode
):
929 bgl
.glEnable(bgl
.GL_BLEND
)
930 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
931 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
933 nodes
, links
= get_nodes_links(context
)
935 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
938 col_outer
= (1.0, 0.2, 0.2, 0.4)
939 col_inner
= (0.0, 0.0, 0.0, 0.5)
940 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
941 elif mode
== "LINKMENU":
942 col_outer
= (0.4, 0.6, 1.0, 0.4)
943 col_inner
= (0.0, 0.0, 0.0, 0.5)
944 col_circle_inner
= (0.08, 0.15, .3, 1.0)
946 col_outer
= (0.2, 1.0, 0.2, 0.4)
947 col_inner
= (0.0, 0.0, 0.0, 0.5)
948 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
950 m1x
= self
.mouse_path
[0][0]
951 m1y
= self
.mouse_path
[0][1]
952 m2x
= self
.mouse_path
[-1][0]
953 m2y
= self
.mouse_path
[-1][1]
955 n1
= nodes
[context
.scene
.NWLazySource
]
956 n2
= nodes
[context
.scene
.NWLazyTarget
]
959 col_outer
= (0.4, 0.4, 0.4, 0.4)
960 col_inner
= (0.0, 0.0, 0.0, 0.5)
961 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
963 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
964 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
965 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
966 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
968 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
969 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
972 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
973 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
976 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
977 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
979 bgl
.glDisable(bgl
.GL_BLEND
)
980 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
982 def get_nodes_links(context
):
983 tree
= context
.space_data
.node_tree
985 # Get nodes from currently edited tree.
986 # If user is editing a group, space_data.node_tree is still the base level (outside group).
987 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
988 # the same as context.active_node, the user is in a group.
989 # Check recursively until we find the real active node_tree:
990 if tree
.nodes
.active
:
991 while tree
.nodes
.active
!= context
.active_node
:
992 tree
= tree
.nodes
.active
.node_tree
994 return tree
.nodes
, tree
.links
997 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
998 base_color
: StringProperty(
1000 default
='diffuse diff albedo base col color',
1001 description
='Naming Components for Base Color maps')
1002 sss_color
: StringProperty(
1003 name
='Subsurface Color',
1004 default
='sss subsurface',
1005 description
='Naming Components for Subsurface Color maps')
1006 metallic
: StringProperty(
1008 default
='metallic metalness metal mtl',
1009 description
='Naming Components for metallness maps')
1010 specular
: StringProperty(
1012 default
='specularity specular spec spc',
1013 description
='Naming Components for Specular maps')
1014 normal
: StringProperty(
1016 default
='normal nor nrm nrml norm',
1017 description
='Naming Components for Normal maps')
1018 bump
: StringProperty(
1021 description
='Naming Components for bump maps')
1022 rough
: StringProperty(
1024 default
='roughness rough rgh',
1025 description
='Naming Components for roughness maps')
1026 gloss
: StringProperty(
1028 default
='gloss glossy glossyness',
1029 description
='Naming Components for glossy maps')
1030 displacement
: StringProperty(
1031 name
='Displacement',
1032 default
='displacement displace disp dsp height heightmap',
1033 description
='Naming Components for displacement maps')
1036 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1037 bl_idname
= __name__
1039 merge_hide
: EnumProperty(
1040 name
="Hide Mix nodes",
1042 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1043 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1044 ("NEVER", "Never", "Never collapse the new merge nodes")
1046 default
='NON_SHADER',
1047 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1048 merge_position
: EnumProperty(
1049 name
="Mix Node Position",
1051 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1052 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1055 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1057 show_hotkey_list
: BoolProperty(
1058 name
="Show Hotkey List",
1060 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1062 hotkey_list_filter
: StringProperty(
1063 name
=" Filter by Name",
1065 description
="Show only hotkeys that have this text in their name"
1067 show_principled_lists
: BoolProperty(
1068 name
="Show Principled naming tags",
1070 description
="Expand this box into a list of all naming tags for principled texture setup"
1072 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1074 def draw(self
, context
):
1075 layout
= self
.layout
1076 col
= layout
.column()
1077 col
.prop(self
, "merge_position")
1078 col
.prop(self
, "merge_hide")
1081 col
= box
.column(align
=True)
1082 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1083 if self
.show_principled_lists
:
1084 tags
= self
.principled_tags
1086 col
.prop(tags
, "base_color")
1087 col
.prop(tags
, "sss_color")
1088 col
.prop(tags
, "metallic")
1089 col
.prop(tags
, "specular")
1090 col
.prop(tags
, "rough")
1091 col
.prop(tags
, "gloss")
1092 col
.prop(tags
, "normal")
1093 col
.prop(tags
, "bump")
1094 col
.prop(tags
, "displacement")
1097 col
= box
.column(align
=True)
1098 hotkey_button_name
= "Show Hotkey List"
1099 if self
.show_hotkey_list
:
1100 hotkey_button_name
= "Hide Hotkey List"
1101 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1102 if self
.show_hotkey_list
:
1103 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1105 for hotkey
in kmi_defs
:
1107 hotkey_name
= hotkey
[7]
1109 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1110 row
= col
.row(align
=True)
1111 row
.label(text
=hotkey_name
)
1112 keystr
= nice_hotkey_name(hotkey
[1])
1114 keystr
= "Shift " + keystr
1116 keystr
= "Alt " + keystr
1118 keystr
= "Ctrl " + keystr
1119 row
.label(text
=keystr
)
1123 def nw_check(context
):
1124 space
= context
.space_data
1125 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree"]
1128 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1135 def poll(cls
, context
):
1136 return nw_check(context
)
1140 class NWLazyMix(Operator
, NWBase
):
1141 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1142 bl_idname
= "node.nw_lazy_mix"
1143 bl_label
= "Mix Nodes"
1144 bl_options
= {'REGISTER', 'UNDO'}
1146 def modal(self
, context
, event
):
1147 context
.area
.tag_redraw()
1148 nodes
, links
= get_nodes_links(context
)
1151 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1154 if not context
.scene
.NWBusyDrawing
:
1155 node1
= node_at_pos(nodes
, context
, event
)
1157 context
.scene
.NWBusyDrawing
= node1
.name
1159 if context
.scene
.NWBusyDrawing
!= 'STOP':
1160 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1162 context
.scene
.NWLazySource
= node1
.name
1163 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1165 if event
.type == 'MOUSEMOVE':
1166 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1168 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1169 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1170 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1173 node2
= node_at_pos(nodes
, context
, event
)
1175 context
.scene
.NWBusyDrawing
= node2
.name
1187 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1189 context
.scene
.NWBusyDrawing
= ""
1192 elif event
.type == 'ESC':
1194 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1195 return {'CANCELLED'}
1197 return {'RUNNING_MODAL'}
1199 def invoke(self
, context
, event
):
1200 if context
.area
.type == 'NODE_EDITOR':
1201 # the arguments we pass the the callback
1202 args
= (self
, context
, 'MIX')
1203 # Add the region OpenGL drawing callback
1204 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1205 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1207 self
.mouse_path
= []
1209 context
.window_manager
.modal_handler_add(self
)
1210 return {'RUNNING_MODAL'}
1212 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1213 return {'CANCELLED'}
1216 class NWLazyConnect(Operator
, NWBase
):
1217 """Connect two nodes without clicking a specific socket (automatically determined"""
1218 bl_idname
= "node.nw_lazy_connect"
1219 bl_label
= "Lazy Connect"
1220 bl_options
= {'REGISTER', 'UNDO'}
1221 with_menu
: BoolProperty()
1223 def modal(self
, context
, event
):
1224 context
.area
.tag_redraw()
1225 nodes
, links
= get_nodes_links(context
)
1228 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1231 if not context
.scene
.NWBusyDrawing
:
1232 node1
= node_at_pos(nodes
, context
, event
)
1234 context
.scene
.NWBusyDrawing
= node1
.name
1236 if context
.scene
.NWBusyDrawing
!= 'STOP':
1237 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1239 context
.scene
.NWLazySource
= node1
.name
1240 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1242 if event
.type == 'MOUSEMOVE':
1243 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1245 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1246 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1247 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1250 node2
= node_at_pos(nodes
, context
, event
)
1252 context
.scene
.NWBusyDrawing
= node2
.name
1257 link_success
= False
1263 if node
.select
== True:
1265 original_sel
.append(node
)
1267 original_unsel
.append(node
)
1271 #link_success = autolink(node1, node2, links)
1273 if len(node1
.outputs
) > 1 and node2
.inputs
:
1274 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1275 elif len(node1
.outputs
) == 1:
1276 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1278 link_success
= autolink(node1
, node2
, links
)
1280 for node
in original_sel
:
1282 for node
in original_unsel
:
1286 force_update(context
)
1287 context
.scene
.NWBusyDrawing
= ""
1290 elif event
.type == 'ESC':
1291 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1292 return {'CANCELLED'}
1294 return {'RUNNING_MODAL'}
1296 def invoke(self
, context
, event
):
1297 if context
.area
.type == 'NODE_EDITOR':
1298 nodes
, links
= get_nodes_links(context
)
1299 node
= node_at_pos(nodes
, context
, event
)
1301 context
.scene
.NWBusyDrawing
= node
.name
1303 # the arguments we pass the the callback
1307 args
= (self
, context
, mode
)
1308 # Add the region OpenGL drawing callback
1309 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1310 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1312 self
.mouse_path
= []
1314 context
.window_manager
.modal_handler_add(self
)
1315 return {'RUNNING_MODAL'}
1317 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1318 return {'CANCELLED'}
1321 class NWDeleteUnused(Operator
, NWBase
):
1322 """Delete all nodes whose output is not used"""
1323 bl_idname
= 'node.nw_del_unused'
1324 bl_label
= 'Delete Unused Nodes'
1325 bl_options
= {'REGISTER', 'UNDO'}
1327 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1328 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1330 def is_unused_node(self
, node
):
1331 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1332 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1333 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1334 if node
.type in end_types
:
1337 for output
in node
.outputs
:
1343 def poll(cls
, context
):
1345 if nw_check(context
):
1346 if context
.space_data
.node_tree
.nodes
:
1350 def execute(self
, context
):
1351 nodes
, links
= get_nodes_links(context
)
1356 if node
.select
== True:
1357 selection
.append(node
.name
)
1363 temp_deleted_nodes
= []
1364 del_unused_iterations
= len(nodes
)
1365 for it
in range(0, del_unused_iterations
):
1366 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1368 if self
.is_unused_node(node
):
1370 deleted_nodes
.append(node
.name
)
1371 bpy
.ops
.node
.delete()
1373 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1376 if self
.delete_frames
:
1384 frames_in_use
.append(node
.parent
)
1386 if node
.type == 'FRAME' and node
not in frames_in_use
:
1389 repeat
= True # repeat for nested frames
1391 if node
not in frames_in_use
:
1393 deleted_nodes
.append(node
.name
)
1394 bpy
.ops
.node
.delete()
1396 if self
.delete_muted
:
1400 deleted_nodes
.append(node
.name
)
1401 bpy
.ops
.node
.delete_reconnect()
1403 # get unique list of deleted nodes (iterations would count the same node more than once)
1404 deleted_nodes
= list(set(deleted_nodes
))
1405 for n
in deleted_nodes
:
1406 self
.report({'INFO'}, "Node " + n
+ " deleted")
1407 num_deleted
= len(deleted_nodes
)
1412 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1414 self
.report({'INFO'}, "Nothing deleted")
1417 nodes
, links
= get_nodes_links(context
)
1419 if node
.name
in selection
:
1423 def invoke(self
, context
, event
):
1424 return context
.window_manager
.invoke_confirm(self
, event
)
1427 class NWSwapLinks(Operator
, NWBase
):
1428 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1429 bl_idname
= 'node.nw_swap_links'
1430 bl_label
= 'Swap Links'
1431 bl_options
= {'REGISTER', 'UNDO'}
1434 def poll(cls
, context
):
1436 if nw_check(context
):
1437 if context
.selected_nodes
:
1438 valid
= len(context
.selected_nodes
) <= 2
1441 def execute(self
, context
):
1442 nodes
, links
= get_nodes_links(context
)
1443 selected_nodes
= context
.selected_nodes
1444 n1
= selected_nodes
[0]
1447 if len(selected_nodes
) == 2:
1448 n2
= selected_nodes
[1]
1449 if n1
.outputs
and n2
.outputs
:
1454 for output
in n1
.outputs
:
1456 for link
in output
.links
:
1457 n1_outputs
.append([out_index
, link
.to_socket
])
1462 for output
in n2
.outputs
:
1464 for link
in output
.links
:
1465 n2_outputs
.append([out_index
, link
.to_socket
])
1469 for connection
in n1_outputs
:
1471 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1473 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1474 for connection
in n2_outputs
:
1476 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1478 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1480 if n1
.outputs
or n2
.outputs
:
1481 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1483 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1486 elif len(selected_nodes
) == 1:
1490 for i1
in n1
.inputs
:
1493 for i2
in n1
.inputs
:
1494 if i1
.type == i2
.type and i2
.is_linked
:
1496 types
.append ([i1
, similar_types
, i
])
1498 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1503 for i2
in n1
.inputs
:
1504 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1506 i1f
= pair
[0].links
[0].from_socket
1507 i1t
= pair
[0].links
[0].to_socket
1508 i2f
= pair
[1].links
[0].from_socket
1509 i2t
= pair
[1].links
[0].to_socket
1514 fs
= t
[0].links
[0].from_socket
1516 links
.remove(t
[0].links
[0])
1517 if i
+1 == len(n1
.inputs
):
1520 while n1
.inputs
[i
].is_linked
:
1522 links
.new(fs
, n1
.inputs
[i
])
1523 elif len(types
) == 2:
1524 i1f
= types
[0][0].links
[0].from_socket
1525 i1t
= types
[0][0].links
[0].to_socket
1526 i2f
= types
[1][0].links
[0].from_socket
1527 i2t
= types
[1][0].links
[0].to_socket
1532 self
.report({'WARNING'}, "This node has no input connections to swap!")
1534 self
.report({'WARNING'}, "This node has no inputs to swap!")
1536 force_update(context
)
1540 class NWResetBG(Operator
, NWBase
):
1541 """Reset the zoom and position of the background image"""
1542 bl_idname
= 'node.nw_bg_reset'
1543 bl_label
= 'Reset Backdrop'
1544 bl_options
= {'REGISTER', 'UNDO'}
1547 def poll(cls
, context
):
1549 if nw_check(context
):
1550 snode
= context
.space_data
1551 valid
= snode
.tree_type
== 'CompositorNodeTree'
1554 def execute(self
, context
):
1555 context
.space_data
.backdrop_zoom
= 1
1556 context
.space_data
.backdrop_offset
[0] = 0
1557 context
.space_data
.backdrop_offset
[1] = 0
1561 class NWAddAttrNode(Operator
, NWBase
):
1562 """Add an Attribute node with this name"""
1563 bl_idname
= 'node.nw_add_attr_node'
1564 bl_label
= 'Add UV map'
1565 bl_options
= {'REGISTER', 'UNDO'}
1567 attr_name
: StringProperty()
1569 def execute(self
, context
):
1570 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1571 nodes
, links
= get_nodes_links(context
)
1572 nodes
.active
.attribute_name
= self
.attr_name
1576 class NWEmissionViewer(Operator
, NWBase
):
1577 bl_idname
= "node.nw_emission_viewer"
1578 bl_label
= "Emission Viewer"
1579 bl_description
= "Connect active node to Emission Shader for shadeless previews"
1580 bl_options
= {'REGISTER', 'UNDO'}
1583 def poll(cls
, context
):
1584 is_cycles
= is_cycles_or_eevee(context
)
1585 if nw_check(context
):
1586 space
= context
.space_data
1587 if space
.tree_type
== 'ShaderNodeTree' and is_cycles
:
1588 if context
.active_node
:
1589 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1595 def invoke(self
, context
, event
):
1596 space
= context
.space_data
1597 shader_type
= space
.shader_type
1598 if shader_type
== 'OBJECT':
1599 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1600 shader_output_type
= "OUTPUT_MATERIAL"
1601 shader_output_ident
= "ShaderNodeOutputMaterial"
1602 shader_viewer_ident
= "ShaderNodeEmission"
1604 shader_output_type
= "OUTPUT_LIGHT"
1605 shader_output_ident
= "ShaderNodeOutputLight"
1606 shader_viewer_ident
= "ShaderNodeEmission"
1608 elif shader_type
== 'WORLD':
1609 shader_output_type
= "OUTPUT_WORLD"
1610 shader_output_ident
= "ShaderNodeOutputWorld"
1611 shader_viewer_ident
= "ShaderNodeBackground"
1612 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1613 mlocx
= event
.mouse_region_x
1614 mlocy
= event
.mouse_region_y
1615 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
1616 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1617 nodes
, links
= get_nodes_links(context
)
1618 in_group
= context
.active_node
!= space
.node_tree
.nodes
.active
1619 active
= nodes
.active
1620 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1623 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
) and not in_group
:
1624 for out
in active
.outputs
:
1629 # get material_output node, store selection, deselect all
1630 materialout
= None # placeholder node
1633 if node
.type == shader_output_type
:
1636 selection
.append(node
.name
)
1639 # get right-most location
1640 sorted_by_xloc
= (sorted(nodes
, key
=lambda x
: x
.location
.x
))
1641 max_xloc_node
= sorted_by_xloc
[-1]
1642 if max_xloc_node
.name
== 'Emission Viewer':
1643 max_xloc_node
= sorted_by_xloc
[-2]
1645 # get average y location
1648 sum_yloc
+= node
.location
.y
1650 new_locx
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1651 new_locy
= sum_yloc
/ len(nodes
)
1653 materialout
= nodes
.new(shader_output_ident
)
1654 materialout
.location
.x
= new_locx
1655 materialout
.location
.y
= new_locy
1656 materialout
.select
= False
1657 # Analyze outputs, add "Emission Viewer" if needed, make links
1660 for i
, out
in enumerate(active
.outputs
):
1662 valid_outputs
.append(i
)
1664 out_i
= valid_outputs
[0] # Start index of node's outputs
1665 for i
, valid_i
in enumerate(valid_outputs
):
1666 for out_link
in active
.outputs
[valid_i
].links
:
1667 if "Emission Viewer" in out_link
.to_node
.name
or (out_link
.to_node
== materialout
and out_link
.to_socket
== materialout
.inputs
[0]):
1668 if i
< len(valid_outputs
) - 1:
1669 out_i
= valid_outputs
[i
+ 1]
1671 out_i
= valid_outputs
[0]
1672 make_links
= [] # store sockets for new links
1674 # If output type not 'SHADER' - "Emission Viewer" needed
1675 if active
.outputs
[out_i
].type != 'SHADER':
1676 # get Emission Viewer node
1677 emission_exists
= False
1678 emission_placeholder
= nodes
[0]
1680 if "Emission Viewer" in node
.name
:
1681 emission_exists
= True
1682 emission_placeholder
= node
1683 if not emission_exists
:
1684 emission
= nodes
.new(shader_viewer_ident
)
1685 emission
.hide
= True
1686 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
1687 emission
.label
= "Viewer"
1688 emission
.name
= "Emission Viewer"
1689 emission
.use_custom_color
= True
1690 emission
.color
= (0.6, 0.5, 0.4)
1691 emission
.select
= False
1693 emission
= emission_placeholder
1694 make_links
.append((active
.outputs
[out_i
], emission
.inputs
[0]))
1696 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
1697 if emission
.outputs
[0].links
.__len
__() > 0:
1698 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
1699 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
1701 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
1703 # Set brightness of viewer to compensate for Film and CM exposure
1704 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
1705 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
1706 emission
.inputs
[1].default_value
= intensity
1709 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
1710 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[1 if active
.outputs
[out_i
].name
== "Volume" else 0]))
1712 if node
.name
== 'Emission Viewer':
1714 bpy
.ops
.node
.delete()
1715 for li_from
, li_to
in make_links
:
1716 links
.new(li_from
, li_to
)
1718 nodes
.active
= active
1720 if node
.name
in selection
:
1722 force_update(context
)
1725 return {'CANCELLED'}
1728 class NWFrameSelected(Operator
, NWBase
):
1729 bl_idname
= "node.nw_frame_selected"
1730 bl_label
= "Frame Selected"
1731 bl_description
= "Add a frame node and parent the selected nodes to it"
1732 bl_options
= {'REGISTER', 'UNDO'}
1734 label_prop
: StringProperty(
1736 description
='The visual name of the frame node',
1739 color_prop
: FloatVectorProperty(
1741 description
="The color of the frame node",
1742 default
=(0.6, 0.6, 0.6),
1743 min=0, max=1, step
=1, precision
=3,
1744 subtype
='COLOR_GAMMA', size
=3
1747 def execute(self
, context
):
1748 nodes
, links
= get_nodes_links(context
)
1751 if node
.select
== True:
1752 selected
.append(node
)
1754 bpy
.ops
.node
.add_node(type='NodeFrame')
1756 frm
.label
= self
.label_prop
1757 frm
.use_custom_color
= True
1758 frm
.color
= self
.color_prop
1760 for node
in selected
:
1766 class NWReloadImages(Operator
, NWBase
):
1767 bl_idname
= "node.nw_reload_images"
1768 bl_label
= "Reload Images"
1769 bl_description
= "Update all the image nodes to match their files on disk"
1771 def execute(self
, context
):
1772 nodes
, links
= get_nodes_links(context
)
1773 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1776 if node
.type in image_types
:
1777 if node
.type == "TEXTURE":
1778 if node
.texture
: # node has texture assigned
1779 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1780 if node
.texture
.image
: # texture has image assigned
1781 node
.texture
.image
.reload()
1789 self
.report({'INFO'}, "Reloaded images")
1790 print("Reloaded " + str(num_reloaded
) + " images")
1791 force_update(context
)
1794 self
.report({'WARNING'}, "No images found to reload in this node tree")
1795 return {'CANCELLED'}
1798 class NWSwitchNodeType(Operator
, NWBase
):
1799 """Switch type of selected nodes """
1800 bl_idname
= "node.nw_swtch_node_type"
1801 bl_label
= "Switch Node Type"
1802 bl_options
= {'REGISTER', 'UNDO'}
1804 to_type
: EnumProperty(
1805 name
="Switch to type",
1806 items
=list(shaders_input_nodes_props
) +
1807 list(shaders_output_nodes_props
) +
1808 list(shaders_shader_nodes_props
) +
1809 list(shaders_texture_nodes_props
) +
1810 list(shaders_color_nodes_props
) +
1811 list(shaders_vector_nodes_props
) +
1812 list(shaders_converter_nodes_props
) +
1813 list(shaders_layout_nodes_props
) +
1814 list(compo_input_nodes_props
) +
1815 list(compo_output_nodes_props
) +
1816 list(compo_color_nodes_props
) +
1817 list(compo_converter_nodes_props
) +
1818 list(compo_filter_nodes_props
) +
1819 list(compo_vector_nodes_props
) +
1820 list(compo_matte_nodes_props
) +
1821 list(compo_distort_nodes_props
) +
1822 list(compo_layout_nodes_props
) +
1823 list(blender_mat_input_nodes_props
) +
1824 list(blender_mat_output_nodes_props
) +
1825 list(blender_mat_color_nodes_props
) +
1826 list(blender_mat_vector_nodes_props
) +
1827 list(blender_mat_converter_nodes_props
) +
1828 list(blender_mat_layout_nodes_props
) +
1829 list(texture_input_nodes_props
) +
1830 list(texture_output_nodes_props
) +
1831 list(texture_color_nodes_props
) +
1832 list(texture_pattern_nodes_props
) +
1833 list(texture_textures_nodes_props
) +
1834 list(texture_converter_nodes_props
) +
1835 list(texture_distort_nodes_props
) +
1836 list(texture_layout_nodes_props
)
1839 def execute(self
, context
):
1840 nodes
, links
= get_nodes_links(context
)
1841 to_type
= self
.to_type
1842 # Those types of nodes will not swap.
1843 src_excludes
= ('NodeFrame')
1844 # Those attributes of nodes will be copied if possible
1845 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
1846 'show_options', 'show_preview', 'show_texture',
1847 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1849 selected
= [n
for n
in nodes
if n
.select
]
1851 for node
in [n
for n
in selected
if
1852 n
.rna_type
.identifier
not in src_excludes
and
1853 n
.rna_type
.identifier
!= to_type
]:
1854 new_node
= nodes
.new(to_type
)
1855 for attr
in attrs_to_pass
:
1856 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
1857 setattr(new_node
, attr
, getattr(node
, attr
))
1858 # set image datablock of dst to image of src
1859 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
1861 new_node
.image
= node
.image
1863 if new_node
.type == 'SWITCH':
1864 new_node
.hide
= True
1865 # Dictionaries: src_sockets and dst_sockets:
1866 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1867 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1868 # in 'INPUTS' and 'OUTPUTS':
1869 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1871 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1873 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1874 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1877 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1878 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1880 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1881 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1882 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1883 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
1884 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1885 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
1886 # enumerate in inputs, then in outputs
1887 # find name, default value and links of socket
1888 for i
, socket
in enumerate(in_out
):
1889 the_name
= socket
.name
1891 # Not every socket, especially in outputs has "default_value"
1892 if hasattr(socket
, 'default_value'):
1893 dval
= socket
.default_value
1895 for lnk
in socket
.links
:
1896 socket_links
.append(lnk
)
1897 # check type of socket to fill proper keys.
1898 for the_type
in types_order_one
:
1899 if socket
.type == the_type
:
1900 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1901 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1902 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
1903 # Check which of the types in inputs/outputs is considered to be "main".
1904 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1905 for type_check
in types_order_one
:
1906 if sockets
[in_out_name
][type_check
]:
1907 sockets
[in_out_name
]['MAIN'] = type_check
1911 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1912 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1915 for inout
, soctype
in (
1916 ('INPUTS', 'MAIN',),
1917 ('INPUTS', 'SHADER',),
1918 ('INPUTS', 'RGBA',),
1919 ('INPUTS', 'VECTOR',),
1920 ('INPUTS', 'VALUE',),
1921 ('OUTPUTS', 'MAIN',),
1922 ('OUTPUTS', 'SHADER',),
1923 ('OUTPUTS', 'RGBA',),
1924 ('OUTPUTS', 'VECTOR',),
1925 ('OUTPUTS', 'VALUE',),
1927 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
1928 if soctype
== 'MAIN':
1929 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
1930 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
1932 sc
= src_sockets
[inout
][soctype
]
1933 dt
= dst_sockets
[inout
][soctype
]
1934 # start with 'dt' to determine number of possibilities.
1935 for i
, soc
in enumerate(dt
):
1936 # if src main has enough entries - match them with dst main sockets by indexes.
1938 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
1939 # add 'VALUE_NAME' criterion to inputs.
1940 if inout
== 'INPUTS' and soctype
== 'VALUE':
1942 if s
[2] == soc
[2]: # if names match
1943 # append src (index, dval), dst (index, dval)
1944 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
1946 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1947 # This creates better links when relinking textures.
1948 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
1949 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
1951 # Pass default values and RELINK:
1952 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1953 # INPUTS: Base on matches in proper order.
1954 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
1956 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
1957 new_node
.inputs
[dst_i
].default_value
= src_dval
1958 # Special case: switch to math
1959 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1960 new_node
.type == 'MATH' and\
1962 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
1963 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
1964 if node
.type == 'MIX_RGB':
1965 if node
.blend_type
in [o
[0] for o
in operations
]:
1966 new_node
.operation
= node
.blend_type
1967 # Special case: switch from math to some types
1968 if node
.type == 'MATH' and\
1969 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1972 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
1973 if new_node
.type == 'MIX_RGB':
1974 if node
.operation
in [t
[0] for t
in blend_types
]:
1975 new_node
.blend_type
= node
.operation
1976 # Set Fac of MIX_RGB to 1.0
1977 new_node
.inputs
[0].default_value
= 1.0
1978 # make link only when dst matching input is not linked already.
1979 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
1980 in_src_link
= node
.inputs
[src_i
].links
[0]
1981 in_dst_socket
= new_node
.inputs
[dst_i
]
1982 links
.new(in_src_link
.from_socket
, in_dst_socket
)
1983 links
.remove(in_src_link
)
1984 # OUTPUTS: Base on matches in proper order.
1985 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
1986 for out_src_link
in node
.outputs
[src_i
].links
:
1987 out_dst_socket
= new_node
.outputs
[dst_i
]
1988 links
.new(out_dst_socket
, out_src_link
.to_socket
)
1989 # relink rest inputs if possible, no criteria
1990 for src_inp
in node
.inputs
:
1991 for dst_inp
in new_node
.inputs
:
1992 if src_inp
.links
and not dst_inp
.links
:
1993 src_link
= src_inp
.links
[0]
1994 links
.new(src_link
.from_socket
, dst_inp
)
1995 links
.remove(src_link
)
1996 # relink rest outputs if possible, base on node kind if any left.
1997 for src_o
in node
.outputs
:
1998 for out_src_link
in src_o
.links
:
1999 for dst_o
in new_node
.outputs
:
2000 if src_o
.type == dst_o
.type:
2001 links
.new(dst_o
, out_src_link
.to_socket
)
2002 # relink rest outputs no criteria if any left. Link all from first output.
2003 for src_o
in node
.outputs
:
2004 for out_src_link
in src_o
.links
:
2005 if new_node
.outputs
:
2006 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2008 force_update(context
)
2012 class NWMergeNodes(Operator
, NWBase
):
2013 bl_idname
= "node.nw_merge_nodes"
2014 bl_label
= "Merge Nodes"
2015 bl_description
= "Merge Selected Nodes"
2016 bl_options
= {'REGISTER', 'UNDO'}
2020 description
="All possible blend types and math operations",
2021 items
=blend_types
+ [op
for op
in operations
if op
not in blend_types
],
2023 merge_type
: EnumProperty(
2025 description
="Type of Merge to be used",
2027 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2028 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2029 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2030 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2031 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2032 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2036 def execute(self
, context
):
2037 settings
= context
.preferences
.addons
[__name__
].preferences
2038 merge_hide
= settings
.merge_hide
2039 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2042 do_hide_shader
= False
2043 if merge_hide
== 'ALWAYS':
2045 do_hide_shader
= True
2046 elif merge_hide
== 'NON_SHADER':
2049 tree_type
= context
.space_data
.node_tree
.type
2050 if tree_type
== 'COMPOSITING':
2051 node_type
= 'CompositorNode'
2052 elif tree_type
== 'SHADER':
2053 node_type
= 'ShaderNode'
2054 elif tree_type
== 'TEXTURE':
2055 node_type
= 'TextureNode'
2056 nodes
, links
= get_nodes_links(context
)
2058 merge_type
= self
.merge_type
2059 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2060 # 'ZCOMBINE' works only if mode == 'MIX'
2061 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2062 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2065 selected_mix
= [] # entry = [index, loc]
2066 selected_shader
= [] # entry = [index, loc]
2067 selected_math
= [] # entry = [index, loc]
2068 selected_z
= [] # entry = [index, loc]
2069 selected_alphaover
= [] # entry = [index, loc]
2071 for i
, node
in enumerate(nodes
):
2072 if node
.select
and node
.outputs
:
2073 if merge_type
== 'AUTO':
2074 for (type, types_list
, dst
) in (
2075 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2076 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2077 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2079 output_type
= node
.outputs
[0].type
2080 valid_mode
= mode
in types_list
2081 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2082 # Cheat that output type is 'RGBA',
2083 # and that 'MIX' exists in math operations list.
2084 # This way when selected_mix list is analyzed:
2085 # Node data will be appended even though it doesn't meet requirements.
2086 if output_type
!= 'SHADER' and mode
== 'MIX':
2087 output_type
= 'RGBA'
2089 if output_type
== type and valid_mode
:
2090 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2092 for (type, types_list
, dst
) in (
2093 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2094 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2095 ('MATH', [t
[0] for t
in operations
], selected_math
),
2096 ('ZCOMBINE', ('MIX', ), selected_z
),
2097 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2099 if merge_type
== type and mode
in types_list
:
2100 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2101 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2102 # use only 'Mix' nodes for merging.
2103 # For that we add selected_math list to selected_mix list and clear selected_math.
2104 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2105 selected_mix
+= selected_math
2108 for nodes_list
in [selected_mix
, selected_shader
, selected_math
, selected_z
, selected_alphaover
]:
2110 count_before
= len(nodes
)
2111 # sort list by loc_x - reversed
2112 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2114 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2115 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2116 if merge_position
== 'CENTER':
2117 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)
2118 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2124 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2128 if nodes_list
== selected_shader
and not do_hide_shader
:
2130 the_range
= len(nodes_list
) - 1
2131 if len(nodes_list
) == 1:
2133 for i
in range(the_range
):
2134 if nodes_list
== selected_mix
:
2135 add_type
= node_type
+ 'MixRGB'
2136 add
= nodes
.new(add_type
)
2137 add
.blend_type
= mode
2139 add
.inputs
[0].default_value
= 1.0
2140 add
.show_preview
= False
2146 add
.width_hidden
= 100.0
2147 elif nodes_list
== selected_math
:
2148 add_type
= node_type
+ 'Math'
2149 add
= nodes
.new(add_type
)
2150 add
.operation
= mode
2156 add
.width_hidden
= 100.0
2157 elif nodes_list
== selected_shader
:
2159 add_type
= node_type
+ 'MixShader'
2160 add
= nodes
.new(add_type
)
2161 add
.hide
= do_hide_shader
2166 add
.width_hidden
= 100.0
2168 add_type
= node_type
+ 'AddShader'
2169 add
= nodes
.new(add_type
)
2170 add
.hide
= do_hide_shader
2175 add
.width_hidden
= 100.0
2176 elif nodes_list
== selected_z
:
2177 add
= nodes
.new('CompositorNodeZcombine')
2178 add
.show_preview
= False
2184 add
.width_hidden
= 100.0
2185 elif nodes_list
== selected_alphaover
:
2186 add
= nodes
.new('CompositorNodeAlphaOver')
2187 add
.show_preview
= False
2193 add
.width_hidden
= 100.0
2194 add
.location
= loc_x
, loc_y
2198 count_after
= len(nodes
)
2199 index
= count_after
- 1
2200 first_selected
= nodes
[nodes_list
[0][0]]
2201 # "last" node has been added as first, so its index is count_before.
2202 last_add
= nodes
[count_before
]
2204 # Two nodes were selected and first selected has no output links, second selected has output links.
2205 # Then add links from last add to all links 'to_socket' of out links of second selected.
2206 if len(nodes_list
) == 2:
2207 if not first_selected
.outputs
[0].links
:
2208 second_selected
= nodes
[nodes_list
[1][0]]
2209 for ss_link
in second_selected
.outputs
[0].links
:
2210 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2211 # Create list of invalid indexes.
2212 invalid_i
= [n
[0] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
)]
2213 # Link only if "to_node" index not in invalid indexes list.
2214 if ss_link
.to_node
not in [nodes
[i
] for i
in invalid_i
]:
2215 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2216 # add links from last_add to all links 'to_socket' of out links of first selected.
2217 for fs_link
in first_selected
.outputs
[0].links
:
2218 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2219 # Create list of invalid indexes.
2220 invalid_i
= [n
[0] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
)]
2221 # Link only if "to_node" index not in invalid indexes list.
2222 if fs_link
.to_node
not in [nodes
[i
] for i
in invalid_i
]:
2223 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2224 # add link from "first" selected and "first" add node
2225 node_to
= nodes
[count_after
- 1]
2226 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2227 if node_to
.type == 'ZCOMBINE':
2228 for fs_out
in first_selected
.outputs
:
2229 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2230 links
.new(fs_out
, node_to
.inputs
[1])
2232 # add links between added ADD nodes and between selected and ADD nodes
2233 for i
in range(count_adds
):
2234 if i
< count_adds
- 1:
2235 node_from
= nodes
[index
]
2236 node_to
= nodes
[index
- 1]
2237 node_to_input_i
= first
2238 node_to_z_i
= 1 # if z combine - link z to first z input
2239 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2240 if node_to
.type == 'ZCOMBINE':
2241 for from_out
in node_from
.outputs
:
2242 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2243 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2244 if len(nodes_list
) > 1:
2245 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2246 node_to
= nodes
[index
]
2247 node_to_input_i
= second
2248 node_to_z_i
= 3 # if z combine - link z to second z input
2249 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2250 if node_to
.type == 'ZCOMBINE':
2251 for from_out
in node_from
.outputs
:
2252 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2253 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2255 # set "last" of added nodes as active
2256 nodes
.active
= last_add
2257 for i
, x
, y
, dx
, h
in nodes_list
:
2258 nodes
[i
].select
= False
2263 class NWBatchChangeNodes(Operator
, NWBase
):
2264 bl_idname
= "node.nw_batch_change"
2265 bl_label
= "Batch Change"
2266 bl_description
= "Batch Change Blend Type and Math Operation"
2267 bl_options
= {'REGISTER', 'UNDO'}
2269 blend_type
: EnumProperty(
2271 items
=blend_types
+ navs
,
2273 operation
: EnumProperty(
2275 items
=operations
+ navs
,
2278 def execute(self
, context
):
2280 nodes
, links
= get_nodes_links(context
)
2281 blend_type
= self
.blend_type
2282 operation
= self
.operation
2283 for node
in context
.selected_nodes
:
2284 if node
.type == 'MIX_RGB':
2285 if not blend_type
in [nav
[0] for nav
in navs
]:
2286 node
.blend_type
= blend_type
2288 if blend_type
== 'NEXT':
2289 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2290 #index = blend_types.index(node.blend_type)
2291 if index
== len(blend_types
) - 1:
2292 node
.blend_type
= blend_types
[0][0]
2294 node
.blend_type
= blend_types
[index
+ 1][0]
2296 if blend_type
== 'PREV':
2297 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2299 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2301 node
.blend_type
= blend_types
[index
- 1][0]
2303 if node
.type == 'MATH':
2304 if not operation
in [nav
[0] for nav
in navs
]:
2305 node
.operation
= operation
2307 if operation
== 'NEXT':
2308 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2309 #index = operations.index(node.operation)
2310 if index
== len(operations
) - 1:
2311 node
.operation
= operations
[0][0]
2313 node
.operation
= operations
[index
+ 1][0]
2315 if operation
== 'PREV':
2316 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2317 #index = operations.index(node.operation)
2319 node
.operation
= operations
[len(operations
) - 1][0]
2321 node
.operation
= operations
[index
- 1][0]
2326 class NWChangeMixFactor(Operator
, NWBase
):
2327 bl_idname
= "node.nw_factor"
2328 bl_label
= "Change Factor"
2329 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2330 bl_options
= {'REGISTER', 'UNDO'}
2332 # option: Change factor.
2333 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2334 # Else - change factor by option value.
2335 option
: FloatProperty()
2337 def execute(self
, context
):
2338 nodes
, links
= get_nodes_links(context
)
2339 option
= self
.option
2340 selected
= [] # entry = index
2341 for si
, node
in enumerate(nodes
):
2343 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2347 fac
= nodes
[si
].inputs
[0]
2348 nodes
[si
].hide
= False
2349 if option
in {0.0, 1.0}:
2350 fac
.default_value
= option
2352 fac
.default_value
+= option
2357 class NWCopySettings(Operator
, NWBase
):
2358 bl_idname
= "node.nw_copy_settings"
2359 bl_label
= "Copy Settings"
2360 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2361 bl_options
= {'REGISTER', 'UNDO'}
2364 def poll(cls
, context
):
2366 if nw_check(context
):
2368 context
.active_node
is not None and
2369 context
.active_node
.type != 'FRAME'
2374 def execute(self
, context
):
2375 node_active
= context
.active_node
2376 node_selected
= context
.selected_nodes
2379 if not (len(node_selected
) > 1):
2380 self
.report({'ERROR'}, "2 nodes must be selected at least")
2381 return {'CANCELLED'}
2383 # Check if active node is in the selection
2384 selected_node_names
= [n
.name
for n
in node_selected
]
2385 if node_active
.name
not in selected_node_names
:
2386 self
.report({'ERROR'}, "No active node")
2387 return {'CANCELLED'}
2389 # Get nodes in selection by type
2390 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2392 if not (len(valid_nodes
) > 1) and node_active
:
2393 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2394 return {'CANCELLED'}
2396 if len(valid_nodes
) != len(node_selected
):
2397 # Report nodes that are not valid
2398 valid_node_names
= [n
.name
for n
in valid_nodes
]
2399 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2400 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2402 # Reference original
2404 #node_selected_names = [n.name for n in node_selected]
2409 # Deselect all nodes
2410 for i
in node_selected
:
2413 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2414 # Run through all other nodes
2415 for node
in valid_nodes
[1:]:
2417 # Check for frame node
2418 parent
= node
.parent
if node
.parent
else None
2419 node_loc
= [node
.location
.x
, node
.location
.y
]
2421 # Select original to duplicate
2424 # Duplicate selected node
2425 bpy
.ops
.node
.duplicate()
2426 new_node
= context
.selected_nodes
[0]
2429 new_node
.select
= False
2431 # Properties to copy
2432 node_tree
= node
.id_data
2433 props_to_copy
= 'bl_idname name location height width'.split(' ')
2437 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2438 for i
in (i
for i
in mappings
if i
.is_linked
):
2440 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2443 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2444 props_to_copy
.pop(0)
2446 for prop
in props_to_copy
:
2447 setattr(new_node
, prop
, props
[prop
])
2449 # Get the node tree to remove the old node
2450 nodes
= node_tree
.nodes
2452 new_node
.name
= props
['name']
2455 new_node
.parent
= parent
2456 new_node
.location
= node_loc
2458 for str_from
, str_to
in reconnections
:
2459 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2461 success_names
.append(new_node
.name
)
2464 node_tree
.nodes
.active
= orig
2465 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2469 class NWCopyLabel(Operator
, NWBase
):
2470 bl_idname
= "node.nw_copy_label"
2471 bl_label
= "Copy Label"
2472 bl_options
= {'REGISTER', 'UNDO'}
2474 option
: EnumProperty(
2476 description
="Source of name of label",
2478 ('FROM_ACTIVE', 'from active', 'from active node',),
2479 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2480 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2484 def execute(self
, context
):
2485 nodes
, links
= get_nodes_links(context
)
2486 option
= self
.option
2487 active
= nodes
.active
2488 if option
== 'FROM_ACTIVE':
2490 src_label
= active
.label
2491 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2492 node
.label
= src_label
2493 elif option
== 'FROM_NODE':
2494 selected
= [n
for n
in nodes
if n
.select
]
2495 for node
in selected
:
2496 for input in node
.inputs
:
2498 src
= input.links
[0].from_node
2499 node
.label
= src
.label
2501 elif option
== 'FROM_SOCKET':
2502 selected
= [n
for n
in nodes
if n
.select
]
2503 for node
in selected
:
2504 for input in node
.inputs
:
2506 src
= input.links
[0].from_socket
2507 node
.label
= src
.name
2513 class NWClearLabel(Operator
, NWBase
):
2514 bl_idname
= "node.nw_clear_label"
2515 bl_label
= "Clear Label"
2516 bl_options
= {'REGISTER', 'UNDO'}
2518 option
: BoolProperty()
2520 def execute(self
, context
):
2521 nodes
, links
= get_nodes_links(context
)
2522 for node
in [n
for n
in nodes
if n
.select
]:
2527 def invoke(self
, context
, event
):
2529 return self
.execute(context
)
2531 return context
.window_manager
.invoke_confirm(self
, event
)
2534 class NWModifyLabels(Operator
, NWBase
):
2535 """Modify Labels of all selected nodes"""
2536 bl_idname
= "node.nw_modify_labels"
2537 bl_label
= "Modify Labels"
2538 bl_options
= {'REGISTER', 'UNDO'}
2540 prepend
: StringProperty(
2541 name
="Add to Beginning"
2543 append
: StringProperty(
2546 replace_from
: StringProperty(
2547 name
="Text to Replace"
2549 replace_to
: StringProperty(
2553 def execute(self
, context
):
2554 nodes
, links
= get_nodes_links(context
)
2555 for node
in [n
for n
in nodes
if n
.select
]:
2556 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
2560 def invoke(self
, context
, event
):
2564 return context
.window_manager
.invoke_props_dialog(self
)
2567 class NWAddTextureSetup(Operator
, NWBase
):
2568 bl_idname
= "node.nw_add_texture"
2569 bl_label
= "Texture Setup"
2570 bl_description
= "Add Texture Node Setup to Selected Shaders"
2571 bl_options
= {'REGISTER', 'UNDO'}
2573 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
2576 def poll(cls
, context
):
2578 if nw_check(context
):
2579 space
= context
.space_data
2580 if space
.tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
2584 def execute(self
, context
):
2585 nodes
, links
= get_nodes_links(context
)
2586 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2587 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
2588 selected_nodes
= [n
for n
in nodes
if n
.select
]
2589 for t_node
in selected_nodes
:
2593 for index
, i
in enumerate(t_node
.inputs
):
2599 locx
= t_node
.location
.x
2600 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
2602 xoffset
= [500, 700]
2604 if t_node
.type in texture_types
+ ['MAPPING']:
2605 xoffset
= [290, 500]
2609 image_type
= 'ShaderNodeTexImage'
2611 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
2612 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
2613 if t_node
.type == 'BACKGROUND':
2614 image_type
= 'ShaderNodeTexEnvironment'
2617 tex
= nodes
.new(image_type
)
2618 tex
.location
= [locx
- 200, locy
+ 112]
2620 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
2622 t_node
.select
= False
2623 if self
.add_mapping
or is_texture
:
2624 if t_node
.type != 'MAPPING':
2625 m
= nodes
.new('ShaderNodeMapping')
2626 m
.location
= [locx
- xoffset
[0], locy
+ 141]
2630 coord
= nodes
.new('ShaderNodeTexCoord')
2631 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
2634 links
.new(m
.outputs
[0], tex
.inputs
[0])
2635 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
2638 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
2639 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
2641 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
2645 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
2646 bl_idname
= "node.nw_add_textures_for_principled"
2647 bl_label
= "Principled Texture Setup"
2648 bl_description
= "Add Texture Node Setup for Principled BSDF"
2649 bl_options
= {'REGISTER', 'UNDO'}
2651 directory
: StringProperty(
2655 description
='Folder to search in for image files'
2657 files
: CollectionProperty(
2658 type=bpy
.types
.OperatorFileListElement
,
2659 options
={'HIDDEN', 'SKIP_SAVE'}
2668 def poll(cls
, context
):
2670 if nw_check(context
):
2671 space
= context
.space_data
2672 if space
.tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
2676 def execute(self
, context
):
2677 # Check if everything is ok
2678 if not self
.directory
:
2679 self
.report({'INFO'}, 'No Folder Selected')
2680 return {'CANCELLED'}
2681 if not self
.files
[:]:
2682 self
.report({'INFO'}, 'No Files Selected')
2683 return {'CANCELLED'}
2685 nodes
, links
= get_nodes_links(context
)
2686 active_node
= nodes
.active
2687 if not active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled':
2688 self
.report({'INFO'}, 'Select Principled BSDF')
2689 return {'CANCELLED'}
2692 def split_into__components(fname
):
2693 # Split filename into components
2694 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2696 fname
= path
.splitext(fname
)[0]
2698 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
2699 # Separate CamelCase by space
2700 fname
= re
.sub("([a-z])([A-Z])","\g<1> \g<2>",fname
)
2701 # Replace common separators with SPACE
2702 seperators
= ['_', '.', '-', '__', '--', '#']
2703 for sep
in seperators
:
2704 fname
= fname
.replace(sep
, ' ')
2706 components
= fname
.split(' ')
2707 components
= [c
.lower() for c
in components
]
2710 # Filter textures names for texturetypes in filenames
2711 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2712 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
2713 normal_abbr
= tags
.normal
.split(' ')
2714 bump_abbr
= tags
.bump
.split(' ')
2715 gloss_abbr
= tags
.gloss
.split(' ')
2716 rough_abbr
= tags
.rough
.split(' ')
2718 ['Displacement', tags
.displacement
.split(' '), None],
2719 ['Base Color', tags
.base_color
.split(' '), None],
2720 ['Subsurface Color', tags
.sss_color
.split(' '), None],
2721 ['Metallic', tags
.metallic
.split(' '), None],
2722 ['Specular', tags
.specular
.split(' '), None],
2723 ['Roughness', rough_abbr
+ gloss_abbr
, None],
2724 ['Normal', normal_abbr
+ bump_abbr
, None],
2727 # Look through texture_types and set value as filename of first matched file
2728 def match_files_to_socket_names():
2729 for sname
in socketnames
:
2730 for file in self
.files
:
2732 filenamecomponents
= split_into__components(fname
)
2733 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
2734 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2739 match_files_to_socket_names()
2740 # Remove socketnames without found files
2741 socketnames
= [s
for s
in socketnames
if s
[2]
2742 and path
.exists(self
.directory
+s
[2])]
2744 self
.report({'INFO'}, 'No matching images found')
2745 print('No matching images found')
2746 return {'CANCELLED'}
2749 print('\nMatched Textures:')
2753 roughness_node
= None
2754 for i
, sname
in enumerate(socketnames
):
2755 print(i
, sname
[0], sname
[2])
2757 # DISPLACEMENT NODES
2758 if sname
[0] == 'Displacement':
2759 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
2760 img
= bpy
.data
.images
.load(self
.directory
+sname
[2])
2761 disp_texture
.image
= img
2762 disp_texture
.label
= 'Displacement'
2763 disp_texture
.color_space
= 'NONE'
2765 # Add displacement offset nodes
2766 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
2767 disp_node
.location
= active_node
.location
+ Vector((0, -560))
2768 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
2770 # TODO Turn on true displacement in the material
2771 # Too complicated for now
2774 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
2776 if not output_node
[0].inputs
[2].is_linked
:
2777 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
2781 if not active_node
.inputs
[sname
[0]].is_linked
:
2782 # No texture node connected -> add texture node with new image
2783 texture_node
= nodes
.new(type='ShaderNodeTexImage')
2784 img
= bpy
.data
.images
.load(self
.directory
+sname
[2])
2785 texture_node
.image
= img
2788 if sname
[0] == 'Normal':
2789 # Test if new texture node is normal or bump map
2790 fname_components
= split_into__components(sname
[2])
2791 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
2792 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
2794 # If Normal add normal node in between
2795 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
2796 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
2798 # If Bump add bump node in between
2799 normal_node
= nodes
.new(type='ShaderNodeBump')
2800 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
2802 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
2803 normal_node_texture
= texture_node
2805 elif sname
[0] == 'Roughness':
2806 # Test if glossy or roughness map
2807 fname_components
= split_into__components(sname
[2])
2808 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
2809 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
2812 # If Roughness nothing to to
2813 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2816 # If Gloss Map add invert node
2817 invert_node
= nodes
.new(type='ShaderNodeInvert')
2818 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
2820 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
2821 roughness_node
= texture_node
2824 # This is a simple connection Texture --> Input slot
2825 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2827 # Use non-color for all but 'Base Color' Textures
2828 if not sname
[0] in ['Base Color']:
2829 texture_node
.color_space
= 'NONE'
2832 # If already texture connected. add to node list for alignment
2833 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
2835 # This are all connected texture nodes
2836 texture_nodes
.append(texture_node
)
2837 texture_node
.label
= sname
[0]
2840 texture_nodes
.append(disp_texture
)
2843 for i
, texture_node
in enumerate(texture_nodes
):
2844 offset
= Vector((-550, (i
* -280) + 200))
2845 texture_node
.location
= active_node
.location
+ offset
2848 # Extra alignment if normal node was added
2849 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
2852 # Alignment of invert node if glossy map
2853 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
2855 # Add texture input + mapping
2856 mapping
= nodes
.new(type='ShaderNodeMapping')
2857 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
2858 if len(texture_nodes
) > 1:
2859 # If more than one texture add reroute node in between
2860 reroute
= nodes
.new(type='NodeReroute')
2861 texture_nodes
.append(reroute
)
2862 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
2863 reroute
.location
= tex_coords
+ Vector((-50, -120))
2864 for texture_node
in texture_nodes
:
2865 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
2866 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
2868 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
2870 # Connect texture_coordiantes to mapping node
2871 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
2872 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
2873 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
2875 # Create frame around tex coords and mapping
2876 frame
= nodes
.new(type='NodeFrame')
2877 frame
.label
= 'Mapping'
2878 mapping
.parent
= frame
2879 texture_input
.parent
= frame
2882 # Create frame around texture nodes
2883 frame
= nodes
.new(type='NodeFrame')
2884 frame
.label
= 'Textures'
2885 for tnode
in texture_nodes
:
2886 tnode
.parent
= frame
2890 active_node
.select
= False
2893 force_update(context
)
2897 class NWAddReroutes(Operator
, NWBase
):
2898 """Add Reroute Nodes and link them to outputs of selected nodes"""
2899 bl_idname
= "node.nw_add_reroutes"
2900 bl_label
= "Add Reroutes"
2901 bl_description
= "Add Reroutes to Outputs"
2902 bl_options
= {'REGISTER', 'UNDO'}
2904 option
: EnumProperty(
2907 ('ALL', 'to all', 'Add to all outputs'),
2908 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2909 ('LINKED', 'to linked', 'Add only to linked outputs'),
2913 def execute(self
, context
):
2914 tree_type
= context
.space_data
.node_tree
.type
2915 option
= self
.option
2916 nodes
, links
= get_nodes_links(context
)
2917 # output valid when option is 'all' or when 'loose' output has no links
2919 post_select
= [] # nodes to be selected after execution
2920 # create reroutes and recreate links
2921 for node
in [n
for n
in nodes
if n
.select
]:
2926 # unhide 'REROUTE' nodes to avoid issues with location.y
2927 if node
.type == 'REROUTE':
2929 # When node is hidden - width_hidden not usable.
2930 # Hack needed to calculate real width
2932 bpy
.ops
.node
.select_all(action
='DESELECT')
2933 helper
= nodes
.new('NodeReroute')
2934 helper
.select
= True
2936 # resize node and helper to zero. Then check locations to calculate width
2937 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2938 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2939 # restore node location
2940 node
.location
= x
, y
2943 # only helper is selected now
2944 bpy
.ops
.node
.delete()
2945 x
= node
.location
.x
+ width
+ 20.0
2946 if node
.type != 'REROUTE':
2950 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
2951 for out_i
, output
in enumerate(node
.outputs
):
2952 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
2953 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
2954 if node
.type != 'R_LAYERS':
2956 else: # if 'R_LAYERS' check if output represent used render pass
2957 node_scene
= node
.scene
2958 node_layer
= node
.layer
2959 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2960 if output
.name
== 'Alpha':
2963 # check entries in global 'rl_outputs' variable
2964 for rlo
in rl_outputs
:
2965 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2966 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
2969 valid
= ((option
== 'ALL') or
2970 (option
== 'LOOSE' and not output
.links
) or
2971 (option
== 'LINKED' and output
.links
))
2972 # Add reroutes only if valid, but offset location in all cases.
2974 n
= nodes
.new('NodeReroute')
2976 for link
in output
.links
:
2977 links
.new(n
.outputs
[0], link
.to_socket
)
2978 links
.new(output
, n
.inputs
[0])
2980 post_select
.append(n
)
2984 # disselect the node so that after execution of script only newly created nodes are selected
2986 # nicer reroutes distribution along y when node.hide
2988 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
2989 for reroute
in [r
for r
in nodes
if r
.select
]:
2990 reroute
.location
.y
-= y_translate
2991 for node
in post_select
:
2997 class NWLinkActiveToSelected(Operator
, NWBase
):
2998 """Link active node to selected nodes basing on various criteria"""
2999 bl_idname
= "node.nw_link_active_to_selected"
3000 bl_label
= "Link Active Node to Selected"
3001 bl_options
= {'REGISTER', 'UNDO'}
3003 replace
: BoolProperty()
3004 use_node_name
: BoolProperty()
3005 use_outputs_names
: BoolProperty()
3008 def poll(cls
, context
):
3010 if nw_check(context
):
3011 if context
.active_node
is not None:
3012 if context
.active_node
.select
:
3016 def execute(self
, context
):
3017 nodes
, links
= get_nodes_links(context
)
3018 replace
= self
.replace
3019 use_node_name
= self
.use_node_name
3020 use_outputs_names
= self
.use_outputs_names
3021 active
= nodes
.active
3022 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3023 outputs
= [] # Only usable outputs of active nodes will be stored here.
3024 for out
in active
.outputs
:
3025 if active
.type != 'R_LAYERS':
3028 # 'R_LAYERS' node type needs special handling.
3029 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3030 # Only outputs that represent used passes should be taken into account
3031 # Check if pass represented by output is used.
3032 # global 'rl_outputs' list will be used for that
3033 for rlo
in rl_outputs
:
3034 pass_used
= False # initial value. Will be set to True if pass is used
3035 if out
.name
== 'Alpha':
3036 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3038 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3039 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3040 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3044 doit
= True # Will be changed to False when links successfully added to previous output.
3047 for node
in selected
:
3048 dst_name
= node
.name
# Will be compared with src_name if needed.
3049 # When node has label - use it as dst_name
3051 dst_name
= node
.label
3052 valid
= True # Initial value. Will be changed to False if names don't match.
3053 src_name
= dst_name
# If names not used - this asignment will keep valid = True.
3055 # Set src_name to source node name or label
3056 src_name
= active
.name
3058 src_name
= active
.label
3059 elif use_outputs_names
:
3060 src_name
= (out
.name
, )
3061 for rlo
in rl_outputs
:
3062 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3063 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3064 if dst_name
not in src_name
:
3067 for input in node
.inputs
:
3068 if input.type == out
.type or node
.type == 'REROUTE':
3069 if replace
or not input.is_linked
:
3070 links
.new(out
, input)
3071 if not use_node_name
and not use_outputs_names
:
3078 class NWAlignNodes(Operator
, NWBase
):
3079 '''Align the selected nodes neatly in a row/column'''
3080 bl_idname
= "node.nw_align_nodes"
3081 bl_label
= "Align Nodes"
3082 bl_options
= {'REGISTER', 'UNDO'}
3083 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3085 def execute(self
, context
):
3086 nodes
, links
= get_nodes_links(context
)
3087 margin
= self
.margin
3091 if node
.select
and node
.type != 'FRAME':
3092 selection
.append(node
)
3094 # If no nodes are selected, align all nodes
3098 elif nodes
.active
in selection
:
3099 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3101 # Check if nodes should be laid out horizontally or vertically
3102 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3103 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3104 x_range
= max(x_locs
) - min(x_locs
)
3105 y_range
= max(y_locs
) - min(y_locs
)
3106 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3107 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3108 horizontal
= x_range
> y_range
3110 # Sort selection by location of node mid-point
3112 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3114 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3118 for node
in selection
:
3119 current_margin
= margin
3120 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3123 node
.location
.x
= current_pos
3124 current_pos
+= current_margin
+ node
.dimensions
.x
3125 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3127 node
.location
.y
= current_pos
3128 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3129 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3131 # If active node is selected, center nodes around it
3132 if active_loc
is not None:
3133 active_loc_diff
= active_loc
- nodes
.active
.location
3134 for node
in selection
:
3135 node
.location
+= active_loc_diff
3136 else: # Position nodes centered around where they used to be
3137 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
])
3138 new_mid
= (max(locs
) + min(locs
)) / 2
3139 for node
in selection
:
3141 node
.location
.x
+= (mid_x
- new_mid
)
3143 node
.location
.y
+= (mid_y
- new_mid
)
3148 class NWSelectParentChildren(Operator
, NWBase
):
3149 bl_idname
= "node.nw_select_parent_child"
3150 bl_label
= "Select Parent or Children"
3151 bl_options
= {'REGISTER', 'UNDO'}
3153 option
: EnumProperty(
3156 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3157 ('CHILD', 'Select Children', 'Select members of selected frame'),
3161 def execute(self
, context
):
3162 nodes
, links
= get_nodes_links(context
)
3163 option
= self
.option
3164 selected
= [node
for node
in nodes
if node
.select
]
3165 if option
== 'PARENT':
3166 for sel
in selected
:
3169 parent
.select
= True
3170 else: # option == 'CHILD'
3171 for sel
in selected
:
3172 children
= [node
for node
in nodes
if node
.parent
== sel
]
3173 for kid
in children
:
3179 class NWDetachOutputs(Operator
, NWBase
):
3180 """Detach outputs of selected node leaving inputs linked"""
3181 bl_idname
= "node.nw_detach_outputs"
3182 bl_label
= "Detach Outputs"
3183 bl_options
= {'REGISTER', 'UNDO'}
3185 def execute(self
, context
):
3186 nodes
, links
= get_nodes_links(context
)
3187 selected
= context
.selected_nodes
3188 bpy
.ops
.node
.duplicate_move_keep_inputs()
3189 new_nodes
= context
.selected_nodes
3190 bpy
.ops
.node
.select_all(action
="DESELECT")
3191 for node
in selected
:
3193 bpy
.ops
.node
.delete_reconnect()
3194 for new_node
in new_nodes
:
3195 new_node
.select
= True
3196 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3201 class NWLinkToOutputNode(Operator
, NWBase
):
3202 """Link to Composite node or Material Output node"""
3203 bl_idname
= "node.nw_link_out"
3204 bl_label
= "Connect to Output"
3205 bl_options
= {'REGISTER', 'UNDO'}
3208 def poll(cls
, context
):
3210 if nw_check(context
):
3211 if context
.active_node
is not None:
3212 for out
in context
.active_node
.outputs
:
3218 def execute(self
, context
):
3219 nodes
, links
= get_nodes_links(context
)
3220 active
= nodes
.active
3223 tree_type
= context
.space_data
.tree_type
3224 output_types_shaders
= [x
[1] for x
in shaders_output_nodes_props
]
3225 output_types_compo
= ['COMPOSITE']
3226 output_types_blender_mat
= ['OUTPUT']
3227 output_types_textures
= ['OUTPUT']
3228 output_types
= output_types_shaders
+ output_types_compo
+ output_types_blender_mat
3230 if node
.type in output_types
:
3234 bpy
.ops
.node
.select_all(action
="DESELECT")
3235 if tree_type
== 'ShaderNodeTree':
3236 if is_cycles_or_eevee(context
):
3237 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3239 output_node
= nodes
.new('ShaderNodeOutput')
3240 elif tree_type
== 'CompositorNodeTree':
3241 output_node
= nodes
.new('CompositorNodeComposite')
3242 elif tree_type
== 'TextureNodeTree':
3243 output_node
= nodes
.new('TextureNodeOutput')
3244 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3245 output_node
.location
.y
= active
.location
.y
3246 if (output_node
and active
.outputs
):
3247 for i
, output
in enumerate(active
.outputs
):
3251 for i
, output
in enumerate(active
.outputs
):
3252 if output
.type == output_node
.inputs
[0].type and not output
.hide
:
3257 if tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
3258 if active
.outputs
[output_index
].name
== 'Volume':
3260 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3262 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3264 force_update(context
) # viewport render does not update
3269 class NWMakeLink(Operator
, NWBase
):
3270 """Make a link from one socket to another"""
3271 bl_idname
= 'node.nw_make_link'
3272 bl_label
= 'Make Link'
3273 bl_options
= {'REGISTER', 'UNDO'}
3274 from_socket
: IntProperty()
3275 to_socket
: IntProperty()
3277 def execute(self
, context
):
3278 nodes
, links
= get_nodes_links(context
)
3280 n1
= nodes
[context
.scene
.NWLazySource
]
3281 n2
= nodes
[context
.scene
.NWLazyTarget
]
3283 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3285 force_update(context
)
3290 class NWCallInputsMenu(Operator
, NWBase
):
3291 """Link from this output"""
3292 bl_idname
= 'node.nw_call_inputs_menu'
3293 bl_label
= 'Make Link'
3294 bl_options
= {'REGISTER', 'UNDO'}
3295 from_socket
: IntProperty()
3297 def execute(self
, context
):
3298 nodes
, links
= get_nodes_links(context
)
3300 context
.scene
.NWSourceSocket
= self
.from_socket
3302 n1
= nodes
[context
.scene
.NWLazySource
]
3303 n2
= nodes
[context
.scene
.NWLazyTarget
]
3304 if len(n2
.inputs
) > 1:
3305 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3306 elif len(n2
.inputs
) == 1:
3307 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3311 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3312 """Add an Image Sequence"""
3313 bl_idname
= 'node.nw_add_sequence'
3314 bl_label
= 'Import Image Sequence'
3315 bl_options
= {'REGISTER', 'UNDO'}
3317 directory
: StringProperty(
3320 filename
: StringProperty(
3323 files
: CollectionProperty(
3324 type=bpy
.types
.OperatorFileListElement
,
3325 options
={'HIDDEN', 'SKIP_SAVE'}
3328 def execute(self
, context
):
3329 nodes
, links
= get_nodes_links(context
)
3330 directory
= self
.directory
3331 filename
= self
.filename
3333 tree
= context
.space_data
.node_tree
3336 # print ("\nDIR:", directory)
3337 # print ("FN:", filename)
3338 # print ("Fs:", list(f.name for f in files), '\n')
3340 if tree
.type == 'SHADER':
3341 node_type
= "ShaderNodeTexImage"
3342 elif tree
.type == 'COMPOSITING':
3343 node_type
= "CompositorNodeImage"
3345 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3346 return {'CANCELLED'}
3348 if not files
[0].name
and not filename
:
3349 self
.report({'ERROR'}, "No file chosen")
3350 return {'CANCELLED'}
3351 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3352 # User has selected multiple files without an active one, or the active one is non-existant
3353 filename
= files
[0].name
3355 if not path
.exists(directory
+filename
):
3356 self
.report({'ERROR'}, filename
+" does not exist!")
3357 return {'CANCELLED'}
3359 without_ext
= '.'.join(filename
.split('.')[:-1])
3361 # if last digit isn't a number, it's not a sequence
3362 if not without_ext
[-1].isdigit():
3363 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3364 return {'CANCELLED'}
3367 extension
= filename
.split('.')[-1]
3368 reverse
= without_ext
[::-1] # reverse string
3371 for char
in reverse
:
3377 without_num
= without_ext
[:count_numbers
*-1]
3379 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3381 num_frames
= len(files
)
3383 nodes_list
= [node
for node
in nodes
]
3385 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3386 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3390 yloc
+= node_mid_pt(node
, 'y')
3391 yloc
= yloc
/len(nodes
)
3396 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3398 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3400 node
.label
= name_with_hashes
3402 img
= bpy
.data
.images
.load(directory
+(without_ext
+'.'+extension
))
3403 img
.source
= 'SEQUENCE'
3404 img
.name
= name_with_hashes
3406 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3407 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
3408 image_user
.frame_duration
= num_frames
3413 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3414 """Add multiple images at once"""
3415 bl_idname
= 'node.nw_add_multiple_images'
3416 bl_label
= 'Open Selected Images'
3417 bl_options
= {'REGISTER', 'UNDO'}
3418 directory
: StringProperty(
3421 files
: CollectionProperty(
3422 type=bpy
.types
.OperatorFileListElement
,
3423 options
={'HIDDEN', 'SKIP_SAVE'}
3426 def execute(self
, context
):
3427 nodes
, links
= get_nodes_links(context
)
3429 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3431 if context
.space_data
.node_tree
.type == 'SHADER':
3432 node_type
= "ShaderNodeTexImage"
3433 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3434 node_type
= "CompositorNodeImage"
3436 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3437 return {'CANCELLED'}
3440 for f
in self
.files
:
3443 node
= nodes
.new(node_type
)
3444 new_nodes
.append(node
)
3447 node
.width_hidden
= 100
3448 node
.location
.x
= xloc
3449 node
.location
.y
= yloc
3452 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3455 # shift new nodes up to center of tree
3456 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3458 if node
in new_nodes
:
3460 node
.location
.y
+= (list_size
/2)
3466 class NWViewerFocus(bpy
.types
.Operator
):
3467 """Set the viewer tile center to the mouse position"""
3468 bl_idname
= "node.nw_viewer_focus"
3469 bl_label
= "Viewer Focus"
3471 x
: bpy
.props
.IntProperty()
3472 y
: bpy
.props
.IntProperty()
3475 def poll(cls
, context
):
3476 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
3478 def execute(self
, context
):
3481 def invoke(self
, context
, event
):
3482 render
= context
.scene
.render
3483 space
= context
.space_data
3484 percent
= render
.resolution_percentage
*0.01
3486 nodes
, links
= get_nodes_links(context
)
3487 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
3490 mlocx
= event
.mouse_region_x
3491 mlocy
= event
.mouse_region_y
3492 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
3494 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
3495 region_x
= context
.region
.width
3496 region_y
= context
.region
.height
3498 region_center_x
= context
.region
.width
/ 2
3499 region_center_y
= context
.region
.height
/ 2
3501 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
3502 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
3504 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_x
3505 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_y
3507 margin_x
= region_center_x
- backdrop_center_x
3508 margin_y
= region_center_y
- backdrop_center_y
3510 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
3511 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
3513 for node
in viewers
:
3514 node
.center_x
= abs_mouse_x
3515 node
.center_y
= abs_mouse_y
3517 return {'PASS_THROUGH'}
3519 return self
.execute(context
)
3522 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
3523 """Save the current viewer node to an image file"""
3524 bl_idname
= "node.nw_save_viewer"
3525 bl_label
= "Save This Image"
3526 filepath
: StringProperty(subtype
="FILE_PATH")
3527 filename_ext
: EnumProperty(
3529 description
="Choose the file format to save to",
3530 items
=(('.bmp', "PNG", ""),
3531 ('.rgb', 'IRIS', ""),
3532 ('.png', 'PNG', ""),
3533 ('.jpg', 'JPEG', ""),
3534 ('.jp2', 'JPEG2000', ""),
3535 ('.tga', 'TARGA', ""),
3536 ('.cin', 'CINEON', ""),
3537 ('.dpx', 'DPX', ""),
3538 ('.exr', 'OPEN_EXR', ""),
3539 ('.hdr', 'HDR', ""),
3540 ('.tif', 'TIFF', "")),
3545 def poll(cls
, context
):
3547 if nw_check(context
):
3548 if context
.space_data
.tree_type
== 'CompositorNodeTree':
3549 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
3550 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
3554 def execute(self
, context
):
3571 basename
, ext
= path
.splitext(fp
)
3572 old_render_format
= context
.scene
.render
.image_settings
.file_format
3573 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
3574 context
.area
.type = "IMAGE_EDITOR"
3575 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
3576 context
.area
.spaces
[0].image
.save_render(fp
)
3577 context
.area
.type = "NODE_EDITOR"
3578 context
.scene
.render
.image_settings
.file_format
= old_render_format
3582 class NWResetNodes(bpy
.types
.Operator
):
3583 """Reset Nodes in Selection"""
3584 bl_idname
= "node.nw_reset_nodes"
3585 bl_label
= "Reset Nodes"
3586 bl_options
= {'REGISTER', 'UNDO'}
3589 def poll(cls
, context
):
3590 space
= context
.space_data
3591 return space
.type == 'NODE_EDITOR'
3593 def execute(self
, context
):
3594 node_active
= context
.active_node
3595 node_selected
= context
.selected_nodes
3596 node_ignore
= ["FRAME","REROUTE", "GROUP"]
3598 # Check if one node is selected at least
3599 if not (len(node_selected
) > 0):
3600 self
.report({'ERROR'}, "1 node must be selected at least")
3601 return {'CANCELLED'}
3603 active_node_name
= node_active
.name
if node_active
.select
else None
3604 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
3606 # Create output lists
3607 selected_node_names
= [n
.name
for n
in node_selected
]
3610 # Reset all valid children in a frame
3611 node_active_is_frame
= False
3612 if len(node_selected
) == 1 and node_active
.type == "FRAME":
3613 node_tree
= node_active
.id_data
3614 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
3616 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
3617 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
3618 node_active_is_frame
= True
3620 # Check if valid nodes in selection
3621 if not (len(valid_nodes
) > 0):
3622 # Check for frames only
3623 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
3624 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
3625 self
.report({'ERROR'}, "Please select only 1 frame to reset")
3627 self
.report({'ERROR'}, "No valid node(s) in selection")
3628 return {'CANCELLED'}
3630 # Report nodes that are not valid
3631 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
3632 valid_node_names
= [n
.name
for n
in valid_nodes
]
3633 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
3634 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
3636 # Deselect all nodes
3637 for i
in node_selected
:
3640 # Run through all valid nodes
3641 for node
in valid_nodes
:
3643 parent
= node
.parent
if node
.parent
else None
3644 node_loc
= [node
.location
.x
, node
.location
.y
]
3646 node_tree
= node
.id_data
3647 props_to_copy
= 'bl_idname name location height width'.split(' ')
3650 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
3651 for i
in (i
for i
in mappings
if i
.is_linked
):
3653 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
3655 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
3657 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
3658 props_to_copy
.pop(0)
3660 for prop
in props_to_copy
:
3661 setattr(new_node
, prop
, props
[prop
])
3663 nodes
= node_tree
.nodes
3665 new_node
.name
= props
['name']
3668 new_node
.parent
= parent
3669 new_node
.location
= node_loc
3671 for str_from
, str_to
in reconnections
:
3672 node_tree
.links
.new(eval(str_from
), eval(str_to
))
3674 new_node
.select
= False
3675 success_names
.append(new_node
.name
)
3677 # Reselect all nodes
3678 if selected_node_names
and node_active_is_frame
is False:
3679 for i
in selected_node_names
:
3680 node_tree
.nodes
[i
].select
= True
3682 if active_node_name
is not None:
3683 node_tree
.nodes
[active_node_name
].select
= True
3684 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
3686 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
3694 def drawlayout(context
, layout
, mode
='non-panel'):
3695 tree_type
= context
.space_data
.tree_type
3697 col
= layout
.column(align
=True)
3698 col
.menu(NWMergeNodesMenu
.bl_idname
)
3701 col
= layout
.column(align
=True)
3702 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
3705 if tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
3706 col
= layout
.column(align
=True)
3707 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
3708 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
3711 col
= layout
.column(align
=True)
3712 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
3713 col
.operator(NWSwapLinks
.bl_idname
)
3714 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
3717 col
= layout
.column(align
=True)
3718 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
3719 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
3722 col
= layout
.column(align
=True)
3724 row
= col
.row(align
=True)
3725 row
.operator(NWClearLabel
.bl_idname
).option
= True
3726 row
.operator(NWModifyLabels
.bl_idname
)
3728 col
.operator(NWClearLabel
.bl_idname
).option
= True
3729 col
.operator(NWModifyLabels
.bl_idname
)
3730 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
3732 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
3735 col
= layout
.column(align
=True)
3736 if tree_type
== 'CompositorNodeTree':
3737 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
3738 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
3741 col
= layout
.column(align
=True)
3742 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
3745 col
= layout
.column(align
=True)
3746 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
3749 col
= layout
.column(align
=True)
3750 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
3754 class NodeWranglerPanel(Panel
, NWBase
):
3755 bl_idname
= "NODE_PT_nw_node_wrangler"
3756 bl_space_type
= 'NODE_EDITOR'
3757 bl_label
= "Node Wrangler"
3758 bl_region_type
= "UI"
3759 bl_category
= "Node Wrangler"
3761 prepend
: StringProperty(
3764 append
: StringProperty()
3765 remove
: StringProperty()
3767 def draw(self
, context
):
3768 self
.layout
.label(text
="(Quick access: Ctrl+Space)")
3769 drawlayout(context
, self
.layout
, mode
='panel')
3775 class NodeWranglerMenu(Menu
, NWBase
):
3776 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
3777 bl_label
= "Node Wrangler"
3779 def draw(self
, context
):
3780 drawlayout(context
, self
.layout
)
3783 class NWMergeNodesMenu(Menu
, NWBase
):
3784 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
3785 bl_label
= "Merge Selected Nodes"
3787 def draw(self
, context
):
3788 type = context
.space_data
.tree_type
3789 layout
= self
.layout
3790 if type == 'ShaderNodeTree' and is_cycles_or_eevee(context
):
3791 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
3792 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
3793 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
3794 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
3796 props
.merge_type
= 'ZCOMBINE'
3797 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
3799 props
.merge_type
= 'ALPHAOVER'
3802 class NWMergeShadersMenu(Menu
, NWBase
):
3803 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
3804 bl_label
= "Merge Selected Nodes using Shaders"
3806 def draw(self
, context
):
3807 layout
= self
.layout
3808 for type in ('MIX', 'ADD'):
3809 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
3811 props
.merge_type
= 'SHADER'
3814 class NWMergeMixMenu(Menu
, NWBase
):
3815 bl_idname
= "NODE_MT_nw_merge_mix_menu"
3816 bl_label
= "Merge Selected Nodes using Mix"
3818 def draw(self
, context
):
3819 layout
= self
.layout
3820 for type, name
, description
in blend_types
:
3821 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3823 props
.merge_type
= 'MIX'
3826 class NWConnectionListOutputs(Menu
, NWBase
):
3827 bl_idname
= "NODE_MT_nw_connection_list_out"
3830 def draw(self
, context
):
3831 layout
= self
.layout
3832 nodes
, links
= get_nodes_links(context
)
3834 n1
= nodes
[context
.scene
.NWLazySource
]
3836 if n1
.type == "R_LAYERS":
3838 for o
in n1
.outputs
:
3839 if o
.enabled
: # Check which passes the render layer has enabled
3840 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
3844 for o
in n1
.outputs
:
3845 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
3849 class NWConnectionListInputs(Menu
, NWBase
):
3850 bl_idname
= "NODE_MT_nw_connection_list_in"
3853 def draw(self
, context
):
3854 layout
= self
.layout
3855 nodes
, links
= get_nodes_links(context
)
3857 n2
= nodes
[context
.scene
.NWLazyTarget
]
3861 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
3862 op
.from_socket
= context
.scene
.NWSourceSocket
3863 op
.to_socket
= index
3867 class NWMergeMathMenu(Menu
, NWBase
):
3868 bl_idname
= "NODE_MT_nw_merge_math_menu"
3869 bl_label
= "Merge Selected Nodes using Math"
3871 def draw(self
, context
):
3872 layout
= self
.layout
3873 for type, name
, description
in operations
:
3874 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3876 props
.merge_type
= 'MATH'
3879 class NWBatchChangeNodesMenu(Menu
, NWBase
):
3880 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
3881 bl_label
= "Batch Change Selected Nodes"
3883 def draw(self
, context
):
3884 layout
= self
.layout
3885 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
3886 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
3889 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
3890 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
3891 bl_label
= "Batch Change Blend Type"
3893 def draw(self
, context
):
3894 layout
= self
.layout
3895 for type, name
, description
in blend_types
:
3896 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3897 props
.blend_type
= type
3898 props
.operation
= 'CURRENT'
3901 class NWBatchChangeOperationMenu(Menu
, NWBase
):
3902 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
3903 bl_label
= "Batch Change Math Operation"
3905 def draw(self
, context
):
3906 layout
= self
.layout
3907 for type, name
, description
in operations
:
3908 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3909 props
.blend_type
= 'CURRENT'
3910 props
.operation
= type
3913 class NWCopyToSelectedMenu(Menu
, NWBase
):
3914 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
3915 bl_label
= "Copy to Selected"
3917 def draw(self
, context
):
3918 layout
= self
.layout
3919 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
3920 layout
.menu(NWCopyLabelMenu
.bl_idname
)
3923 class NWCopyLabelMenu(Menu
, NWBase
):
3924 bl_idname
= "NODE_MT_nw_copy_label_menu"
3925 bl_label
= "Copy Label"
3927 def draw(self
, context
):
3928 layout
= self
.layout
3929 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
3930 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
3931 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
3934 class NWAddReroutesMenu(Menu
, NWBase
):
3935 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
3936 bl_label
= "Add Reroutes"
3937 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
3939 def draw(self
, context
):
3940 layout
= self
.layout
3941 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
3942 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
3943 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
3946 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
3947 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
3948 bl_label
= "Link Active to Selected"
3950 def draw(self
, context
):
3951 layout
= self
.layout
3952 layout
.menu(NWLinkStandardMenu
.bl_idname
)
3953 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
3954 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
3957 class NWLinkStandardMenu(Menu
, NWBase
):
3958 bl_idname
= "NODE_MT_nw_link_standard_menu"
3959 bl_label
= "To All Selected"
3961 def draw(self
, context
):
3962 layout
= self
.layout
3963 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
3964 props
.replace
= False
3965 props
.use_node_name
= False
3966 props
.use_outputs_names
= False
3967 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
3968 props
.replace
= True
3969 props
.use_node_name
= False
3970 props
.use_outputs_names
= False
3973 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
3974 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
3975 bl_label
= "Use Node Name/Label"
3977 def draw(self
, context
):
3978 layout
= self
.layout
3979 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
3980 props
.replace
= False
3981 props
.use_node_name
= True
3982 props
.use_outputs_names
= False
3983 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
3984 props
.replace
= True
3985 props
.use_node_name
= True
3986 props
.use_outputs_names
= False
3989 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
3990 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
3991 bl_label
= "Use Outputs Names"
3993 def draw(self
, context
):
3994 layout
= self
.layout
3995 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
3996 props
.replace
= False
3997 props
.use_node_name
= False
3998 props
.use_outputs_names
= True
3999 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4000 props
.replace
= True
4001 props
.use_node_name
= False
4002 props
.use_outputs_names
= True
4005 class NWVertColMenu(bpy
.types
.Menu
):
4006 bl_idname
= "NODE_MT_nw_node_vertex_color_menu"
4007 bl_label
= "Vertex Colors"
4010 def poll(cls
, context
):
4012 if nw_check(context
):
4013 snode
= context
.space_data
4014 valid
= snode
.tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
)
4017 def draw(self
, context
):
4019 nodes
, links
= get_nodes_links(context
)
4020 mat
= context
.object.active_material
4023 for obj
in bpy
.data
.objects
:
4024 for slot
in obj
.material_slots
:
4025 if slot
.material
== mat
:
4029 if obj
.data
.vertex_colors
:
4030 for vcol
in obj
.data
.vertex_colors
:
4031 vcols
.append(vcol
.name
)
4032 vcols
= list(set(vcols
)) # get a unique list
4036 l
.operator(NWAddAttrNode
.bl_idname
, text
=vcol
).attr_name
= vcol
4038 l
.label("No Vertex Color layers on objects with this material")
4041 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4042 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4043 bl_label
= "Switch Type to..."
4045 def draw(self
, context
):
4046 layout
= self
.layout
4047 tree
= context
.space_data
.node_tree
4048 if tree
.type == 'SHADER':
4049 if is_cycles_or_eevee(context
):
4050 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4051 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4052 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4053 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4054 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4055 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4056 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4057 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4059 layout
.menu(NWSwitchMatInputSubmenu
.bl_idname
)
4060 layout
.menu(NWSwitchMatOutputSubmenu
.bl_idname
)
4061 layout
.menu(NWSwitchMatColorSubmenu
.bl_idname
)
4062 layout
.menu(NWSwitchMatVectorSubmenu
.bl_idname
)
4063 layout
.menu(NWSwitchMatConverterSubmenu
.bl_idname
)
4064 layout
.menu(NWSwitchMatLayoutSubmenu
.bl_idname
)
4065 if tree
.type == 'COMPOSITING':
4066 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4067 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4068 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4069 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4070 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4071 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4072 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4073 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4074 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4075 if tree
.type == 'TEXTURE':
4076 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4077 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4078 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4079 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4080 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4081 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4082 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4083 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4086 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4087 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4090 def draw(self
, context
):
4091 layout
= self
.layout
4092 for ident
, node_type
, rna_name
in sorted(shaders_input_nodes_props
, key
=lambda k
: k
[2]):
4093 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4094 props
.to_type
= ident
4097 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4098 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4101 def draw(self
, context
):
4102 layout
= self
.layout
4103 for ident
, node_type
, rna_name
in sorted(shaders_output_nodes_props
, key
=lambda k
: k
[2]):
4104 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4105 props
.to_type
= ident
4108 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4109 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4112 def draw(self
, context
):
4113 layout
= self
.layout
4114 for ident
, node_type
, rna_name
in sorted(shaders_shader_nodes_props
, key
=lambda k
: k
[2]):
4115 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4116 props
.to_type
= ident
4119 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4120 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4121 bl_label
= "Texture"
4123 def draw(self
, context
):
4124 layout
= self
.layout
4125 for ident
, node_type
, rna_name
in sorted(shaders_texture_nodes_props
, key
=lambda k
: k
[2]):
4126 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4127 props
.to_type
= ident
4130 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4131 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4134 def draw(self
, context
):
4135 layout
= self
.layout
4136 for ident
, node_type
, rna_name
in sorted(shaders_color_nodes_props
, key
=lambda k
: k
[2]):
4137 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4138 props
.to_type
= ident
4141 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4142 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4145 def draw(self
, context
):
4146 layout
= self
.layout
4147 for ident
, node_type
, rna_name
in sorted(shaders_vector_nodes_props
, key
=lambda k
: k
[2]):
4148 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4149 props
.to_type
= ident
4152 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4153 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4154 bl_label
= "Converter"
4156 def draw(self
, context
):
4157 layout
= self
.layout
4158 for ident
, node_type
, rna_name
in sorted(shaders_converter_nodes_props
, key
=lambda k
: k
[2]):
4159 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4160 props
.to_type
= ident
4163 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4164 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4167 def draw(self
, context
):
4168 layout
= self
.layout
4169 for ident
, node_type
, rna_name
in sorted(shaders_layout_nodes_props
, key
=lambda k
: k
[2]):
4170 if node_type
!= 'FRAME':
4171 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4172 props
.to_type
= ident
4175 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4176 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4179 def draw(self
, context
):
4180 layout
= self
.layout
4181 for ident
, node_type
, rna_name
in sorted(compo_input_nodes_props
, key
=lambda k
: k
[2]):
4182 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4183 props
.to_type
= ident
4186 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4187 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4190 def draw(self
, context
):
4191 layout
= self
.layout
4192 for ident
, node_type
, rna_name
in sorted(compo_output_nodes_props
, key
=lambda k
: k
[2]):
4193 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4194 props
.to_type
= ident
4197 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4198 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4201 def draw(self
, context
):
4202 layout
= self
.layout
4203 for ident
, node_type
, rna_name
in sorted(compo_color_nodes_props
, key
=lambda k
: k
[2]):
4204 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4205 props
.to_type
= ident
4208 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4209 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4210 bl_label
= "Converter"
4212 def draw(self
, context
):
4213 layout
= self
.layout
4214 for ident
, node_type
, rna_name
in sorted(compo_converter_nodes_props
, key
=lambda k
: k
[2]):
4215 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4216 props
.to_type
= ident
4219 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4220 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4223 def draw(self
, context
):
4224 layout
= self
.layout
4225 for ident
, node_type
, rna_name
in sorted(compo_filter_nodes_props
, key
=lambda k
: k
[2]):
4226 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4227 props
.to_type
= ident
4230 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4231 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4234 def draw(self
, context
):
4235 layout
= self
.layout
4236 for ident
, node_type
, rna_name
in sorted(compo_vector_nodes_props
, key
=lambda k
: k
[2]):
4237 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4238 props
.to_type
= ident
4241 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4242 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4245 def draw(self
, context
):
4246 layout
= self
.layout
4247 for ident
, node_type
, rna_name
in sorted(compo_matte_nodes_props
, key
=lambda k
: k
[2]):
4248 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4249 props
.to_type
= ident
4252 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4253 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4254 bl_label
= "Distort"
4256 def draw(self
, context
):
4257 layout
= self
.layout
4258 for ident
, node_type
, rna_name
in sorted(compo_distort_nodes_props
, key
=lambda k
: k
[2]):
4259 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4260 props
.to_type
= ident
4263 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4264 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4267 def draw(self
, context
):
4268 layout
= self
.layout
4269 for ident
, node_type
, rna_name
in sorted(compo_layout_nodes_props
, key
=lambda k
: k
[2]):
4270 if node_type
!= 'FRAME':
4271 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4272 props
.to_type
= ident
4275 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4276 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4279 def draw(self
, context
):
4280 layout
= self
.layout
4281 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4282 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4283 props
.to_type
= ident
4286 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4287 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4290 def draw(self
, context
):
4291 layout
= self
.layout
4292 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4293 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4294 props
.to_type
= ident
4297 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4298 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4301 def draw(self
, context
):
4302 layout
= self
.layout
4303 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4304 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4305 props
.to_type
= ident
4308 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4309 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4312 def draw(self
, context
):
4313 layout
= self
.layout
4314 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4315 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4316 props
.to_type
= ident
4319 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4320 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4321 bl_label
= "Converter"
4323 def draw(self
, context
):
4324 layout
= self
.layout
4325 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4326 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4327 props
.to_type
= ident
4330 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4331 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4334 def draw(self
, context
):
4335 layout
= self
.layout
4336 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4337 if node_type
!= 'FRAME':
4338 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4339 props
.to_type
= ident
4342 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4343 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4346 def draw(self
, context
):
4347 layout
= self
.layout
4348 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4349 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4350 props
.to_type
= ident
4353 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4354 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4357 def draw(self
, context
):
4358 layout
= self
.layout
4359 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4360 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4361 props
.to_type
= ident
4364 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4365 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4368 def draw(self
, context
):
4369 layout
= self
.layout
4370 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4371 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4372 props
.to_type
= ident
4375 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4376 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4377 bl_label
= "Pattern"
4379 def draw(self
, context
):
4380 layout
= self
.layout
4381 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4382 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4383 props
.to_type
= ident
4386 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4387 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4388 bl_label
= "Textures"
4390 def draw(self
, context
):
4391 layout
= self
.layout
4392 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4393 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4394 props
.to_type
= ident
4397 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4398 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4399 bl_label
= "Converter"
4401 def draw(self
, context
):
4402 layout
= self
.layout
4403 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4404 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4405 props
.to_type
= ident
4408 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4409 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4410 bl_label
= "Distort"
4412 def draw(self
, context
):
4413 layout
= self
.layout
4414 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4415 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4416 props
.to_type
= ident
4419 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4420 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4423 def draw(self
, context
):
4424 layout
= self
.layout
4425 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
4426 if node_type
!= 'FRAME':
4427 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4428 props
.to_type
= ident
4432 # APPENDAGES TO EXISTING UI
4436 def select_parent_children_buttons(self
, context
):
4437 layout
= self
.layout
4438 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
4439 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
4442 def attr_nodes_menu_func(self
, context
):
4443 col
= self
.layout
.column(align
=True)
4444 col
.menu("NODE_MT_nw_node_vertex_color_menu")
4448 def multipleimages_menu_func(self
, context
):
4449 col
= self
.layout
.column(align
=True)
4450 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
4451 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
4455 def bgreset_menu_func(self
, context
):
4456 self
.layout
.operator(NWResetBG
.bl_idname
)
4459 def save_viewer_menu_func(self
, context
):
4460 if nw_check(context
):
4461 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4462 if context
.scene
.node_tree
.nodes
.active
:
4463 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
4464 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
4467 def reset_nodes_button(self
, context
):
4468 node_active
= context
.active_node
4469 node_selected
= context
.selected_nodes
4470 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4472 # Check if active node is in the selection and respective type
4473 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
4474 row
= self
.layout
.row()
4475 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
4476 self
.layout
.separator()
4478 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
4479 row
= self
.layout
.row()
4480 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
4481 self
.layout
.separator()
4485 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4489 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4490 # props entry: (property name, property value)
4493 # NWMergeNodes with Ctrl (AUTO).
4494 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
4495 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4496 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
4497 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4498 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4499 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4500 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
4501 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4502 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4503 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4504 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
4505 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4506 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4507 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4508 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
4509 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4510 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4511 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4512 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
4513 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4514 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
4515 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4516 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
4517 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4518 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4519 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4520 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4521 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
4522 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4523 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
4524 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4525 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4526 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4527 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
4528 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4529 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4530 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4531 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
4532 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4533 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4534 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4535 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
4536 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4537 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4538 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4539 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
4540 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4541 # NWMergeNodes with Ctrl Shift (MATH)
4542 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4543 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4544 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
4545 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4546 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4547 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4548 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
4549 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4550 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4551 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4552 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
4553 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4554 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4555 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4556 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
4557 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4558 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
4559 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4560 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
4561 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4562 # BATCH CHANGE NODES
4563 # NWBatchChangeNodes with Alt
4564 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
4565 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4566 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
4567 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4568 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4569 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4570 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
4571 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4572 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4573 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4574 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
4575 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4576 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4577 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4578 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
4579 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4580 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4581 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4582 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
4583 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4584 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
4585 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4586 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
4587 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4588 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
4589 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4590 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
4591 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4592 # LINK ACTIVE TO SELECTED
4593 # Don't use names, don't replace links (K)
4594 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
4595 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4596 # Don't use names, replace links (Shift K)
4597 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
4598 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4599 # Use node name, don't replace links (')
4600 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
4601 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4602 # Use node name, replace links (Shift ')
4603 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
4604 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4605 # Don't use names, don't replace links (;)
4606 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
4607 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4608 # Don't use names, replace links (')
4609 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
4610 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4612 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4613 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4614 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4615 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4616 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4617 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4618 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4619 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4620 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4621 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4622 # CLEAR LABEL (Alt L)
4623 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4624 # MODIFY LABEL (Alt Shift L)
4625 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4626 # Copy Label from active to selected
4627 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4628 # DETACH OUTPUTS (Alt Shift D)
4629 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4630 # LINK TO OUTPUT NODE (O)
4631 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4632 # SELECT PARENT/CHILDREN
4634 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4636 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4638 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4639 # Add Principled BSDF Texture Setup
4640 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4642 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4644 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4646 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4648 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4650 (NWEmissionViewer
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4652 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
4654 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, None, "Lazy Mix"),
4656 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, False, False, (('with_menu', False),), "Lazy Connect"),
4657 # Lazy Connect with Menu
4658 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4659 # Viewer Tile Center
4660 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4662 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4663 # Reset Nodes (Back Space)
4664 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4666 ('wm.call_menu', 'SPACE', 'PRESS', True, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
4667 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4668 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4669 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
4670 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
4671 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
4676 NWPrincipledPreferences
,
4696 NWAddPrincipledSetup
,
4698 NWLinkActiveToSelected
,
4700 NWSelectParentChildren
,
4706 NWAddMultipleImages
,
4715 NWConnectionListOutputs
,
4716 NWConnectionListInputs
,
4718 NWBatchChangeNodesMenu
,
4719 NWBatchChangeBlendTypeMenu
,
4720 NWBatchChangeOperationMenu
,
4721 NWCopyToSelectedMenu
,
4724 NWLinkActiveToSelectedMenu
,
4726 NWLinkUseNodeNameMenu
,
4727 NWLinkUseOutputsNamesMenu
,
4729 NWSwitchNodeTypeMenu
,
4730 NWSwitchShadersInputSubmenu
,
4731 NWSwitchShadersOutputSubmenu
,
4732 NWSwitchShadersShaderSubmenu
,
4733 NWSwitchShadersTextureSubmenu
,
4734 NWSwitchShadersColorSubmenu
,
4735 NWSwitchShadersVectorSubmenu
,
4736 NWSwitchShadersConverterSubmenu
,
4737 NWSwitchShadersLayoutSubmenu
,
4738 NWSwitchCompoInputSubmenu
,
4739 NWSwitchCompoOutputSubmenu
,
4740 NWSwitchCompoColorSubmenu
,
4741 NWSwitchCompoConverterSubmenu
,
4742 NWSwitchCompoFilterSubmenu
,
4743 NWSwitchCompoVectorSubmenu
,
4744 NWSwitchCompoMatteSubmenu
,
4745 NWSwitchCompoDistortSubmenu
,
4746 NWSwitchCompoLayoutSubmenu
,
4747 NWSwitchMatInputSubmenu
,
4748 NWSwitchMatOutputSubmenu
,
4749 NWSwitchMatColorSubmenu
,
4750 NWSwitchMatVectorSubmenu
,
4751 NWSwitchMatConverterSubmenu
,
4752 NWSwitchMatLayoutSubmenu
,
4753 NWSwitchTexInputSubmenu
,
4754 NWSwitchTexOutputSubmenu
,
4755 NWSwitchTexColorSubmenu
,
4756 NWSwitchTexPatternSubmenu
,
4757 NWSwitchTexTexturesSubmenu
,
4758 NWSwitchTexConverterSubmenu
,
4759 NWSwitchTexDistortSubmenu
,
4760 NWSwitchTexLayoutSubmenu
,
4764 from bpy
.utils
import register_class
4767 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
4768 name
="Busy Drawing!",
4770 description
="An internal property used to store only the first mouse position")
4771 bpy
.types
.Scene
.NWLazySource
= StringProperty(
4772 name
="Lazy Source!",
4774 description
="An internal property used to store the first node in a Lazy Connect operation")
4775 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
4776 name
="Lazy Target!",
4778 description
="An internal property used to store the last node in a Lazy Connect operation")
4779 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
4780 name
="Source Socket!",
4782 description
="An internal property used to store the source socket in a Lazy Connect operation")
4788 addon_keymaps
.clear()
4789 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
4791 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
4792 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
4793 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
4795 for prop
, value
in props
:
4796 setattr(kmi
.properties
, prop
, value
)
4797 addon_keymaps
.append((km
, kmi
))
4800 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
4801 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
4802 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
4803 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
4804 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
4805 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
4806 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
4807 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
4811 from bpy
.utils
import unregister_class
4814 del bpy
.types
.Scene
.NWBusyDrawing
4815 del bpy
.types
.Scene
.NWLazySource
4816 del bpy
.types
.Scene
.NWLazyTarget
4817 del bpy
.types
.Scene
.NWSourceSocket
4820 for km
, kmi
in addon_keymaps
:
4821 km
.keymap_items
.remove(kmi
)
4822 addon_keymaps
.clear()
4825 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
4826 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
4827 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
4828 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
4829 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
4830 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
4831 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
4832 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
4835 unregister_class(cls
)
4837 if __name__
== "__main__":