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 Shift-W",
25 "description": "Various tools to enhance and speed up node-based workflow",
27 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
28 "node/node_wrangler.html",
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'}
550 def is_visible_socket(socket
):
551 return not socket
.hide
and socket
.enabled
553 def nice_hotkey_name(punc
):
554 # convert the ugly string name into the actual character
556 ('LEFTMOUSE', "LMB"),
557 ('MIDDLEMOUSE', "MMB"),
558 ('RIGHTMOUSE', "RMB"),
559 ('WHEELUPMOUSE', "Wheel Up"),
560 ('WHEELDOWNMOUSE', "Wheel Down"),
561 ('WHEELINMOUSE', "Wheel In"),
562 ('WHEELOUTMOUSE', "Wheel Out"),
575 ('LINE_FEED', "Enter"),
582 ('BACK_SLASH', "\\"),
584 ('NUMPAD_1', "Numpad 1"),
585 ('NUMPAD_2', "Numpad 2"),
586 ('NUMPAD_3', "Numpad 3"),
587 ('NUMPAD_4', "Numpad 4"),
588 ('NUMPAD_5', "Numpad 5"),
589 ('NUMPAD_6', "Numpad 6"),
590 ('NUMPAD_7', "Numpad 7"),
591 ('NUMPAD_8', "Numpad 8"),
592 ('NUMPAD_9', "Numpad 9"),
593 ('NUMPAD_0', "Numpad 0"),
594 ('NUMPAD_PERIOD', "Numpad ."),
595 ('NUMPAD_SLASH', "Numpad /"),
596 ('NUMPAD_ASTERIX', "Numpad *"),
597 ('NUMPAD_MINUS', "Numpad -"),
598 ('NUMPAD_ENTER', "Numpad Enter"),
599 ('NUMPAD_PLUS', "Numpad +"),
602 for (ugly
, nice
) in pairs
:
607 nice_punc
= punc
.replace("_", " ").title()
611 def force_update(context
):
612 context
.space_data
.node_tree
.update_tag()
616 prefs
= bpy
.context
.preferences
.system
617 return prefs
.dpi
* prefs
.pixel_size
/ 72
620 def node_mid_pt(node
, axis
):
622 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
624 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
630 def autolink(node1
, node2
, links
):
633 for outp
in node1
.outputs
:
634 for inp
in node2
.inputs
:
635 if not inp
.is_linked
and inp
.name
== outp
.name
:
640 for outp
in node1
.outputs
:
641 for inp
in node2
.inputs
:
642 if not inp
.is_linked
and inp
.type == outp
.type:
647 # force some connection even if the type doesn't match
648 for outp
in node1
.outputs
:
649 for inp
in node2
.inputs
:
650 if not inp
.is_linked
:
655 # even if no sockets are open, force one of matching type
656 for outp
in node1
.outputs
:
657 for inp
in node2
.inputs
:
658 if inp
.type == outp
.type:
664 for outp
in node1
.outputs
:
665 for inp
in node2
.inputs
:
670 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
674 def node_at_pos(nodes
, context
, event
):
675 nodes_near_mouse
= []
676 nodes_under_mouse
= []
679 store_mouse_cursor(context
, event
)
680 x
, y
= context
.space_data
.cursor_location
684 # Make a list of each corner (and middle of border) for each node.
685 # Will be sorted to find nearest point and thus nearest node
686 node_points_with_dist
= []
689 if node
.type != 'FRAME': # no point trying to link to a frame node
690 locx
= node
.location
.x
691 locy
= node
.location
.y
692 dimx
= node
.dimensions
.x
/dpifac()
693 dimy
= node
.dimensions
.y
/dpifac()
695 locx
+= node
.parent
.location
.x
696 locy
+= node
.parent
.location
.y
697 if node
.parent
.parent
:
698 locx
+= node
.parent
.parent
.location
.x
699 locy
+= node
.parent
.parent
.location
.y
700 if node
.parent
.parent
.parent
:
701 locx
+= node
.parent
.parent
.parent
.location
.x
702 locy
+= node
.parent
.parent
.parent
.location
.y
703 if node
.parent
.parent
.parent
.parent
:
704 # Support three levels or parenting
705 # There's got to be a better way to do this...
708 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
709 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
710 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
711 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
713 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
714 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
715 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
716 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
718 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
721 if node
.type != 'FRAME' and skipnode
== False:
722 locx
= node
.location
.x
723 locy
= node
.location
.y
724 dimx
= node
.dimensions
.x
/dpifac()
725 dimy
= node
.dimensions
.y
/dpifac()
727 locx
+= node
.parent
.location
.x
728 locy
+= node
.parent
.location
.y
729 if (locx
<= x
<= locx
+ dimx
) and \
730 (locy
- dimy
<= y
<= locy
):
731 nodes_under_mouse
.append(node
)
733 if len(nodes_under_mouse
) == 1:
734 if nodes_under_mouse
[0] != nearest_node
:
735 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
737 target_node
= nearest_node
# else use the nearest node
739 target_node
= nearest_node
743 def store_mouse_cursor(context
, event
):
744 space
= context
.space_data
745 v2d
= context
.region
.view2d
746 tree
= space
.edit_tree
748 # convert mouse position to the View2D for later node placement
749 if context
.region
.type == 'WINDOW':
750 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
752 space
.cursor_location
= tree
.view_center
754 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
755 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
757 vertices
= ((x1
, y1
), (x2
, y2
))
758 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
759 colour
[1]+(1.0-colour
[1])/4,
760 colour
[2]+(1.0-colour
[2])/4,
761 colour
[3]+(1.0-colour
[3])/4),
764 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
765 bgl
.glLineWidth(size
* dpifac())
771 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
772 radius
= radius
* dpifac()
774 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
775 radius
* sin(i
* 2 * pi
/ sides
) + my
)
776 for i
in range(sides
+ 1)]
778 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
780 shader
.uniform_float("color", colour
)
783 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
784 area_width
= bpy
.context
.area
.width
- (16*dpifac()) - 1
785 bottom_bar
= (16*dpifac()) + 1
787 radius
= radius
*dpifac()
789 nlocx
= (node
.location
.x
+1)*dpifac()
790 nlocy
= (node
.location
.y
+1)*dpifac()
791 ndimx
= node
.dimensions
.x
792 ndimy
= node
.dimensions
.y
793 # This is a stupid way to do this... TODO use while loop
795 nlocx
+= node
.parent
.location
.x
796 nlocy
+= node
.parent
.location
.y
797 if node
.parent
.parent
:
798 nlocx
+= node
.parent
.parent
.location
.x
799 nlocy
+= node
.parent
.parent
.location
.y
800 if node
.parent
.parent
.parent
:
801 nlocx
+= node
.parent
.parent
.parent
.location
.x
802 nlocy
+= node
.parent
.parent
.parent
.location
.y
807 if node
.type == 'REROUTE':
815 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
817 for i
in range(sides
+1):
819 if my
> bottom_bar
and mx
< area_width
:
820 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
821 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
822 vertices
.append((cosine
,sine
))
823 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
825 shader
.uniform_float("color", colour
)
829 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
831 for i
in range(sides
+1):
833 if my
> bottom_bar
and mx
< area_width
:
834 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
835 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
836 vertices
.append((cosine
,sine
))
837 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
839 shader
.uniform_float("color", colour
)
843 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
845 for i
in range(sides
+1):
847 if my
> bottom_bar
and mx
< area_width
:
848 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
849 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
850 vertices
.append((cosine
,sine
))
851 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
853 shader
.uniform_float("color", colour
)
856 # Bottom right corner
857 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
859 for i
in range(sides
+1):
861 if my
> bottom_bar
and mx
< area_width
:
862 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
863 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
864 vertices
.append((cosine
,sine
))
865 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
867 shader
.uniform_float("color", colour
)
870 # prepare drawing all edges in one batch
876 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
877 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
878 if m1x
< area_width
and m2x
< area_width
:
879 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
880 (m1x
,m1y
), (m1x
-radius
,m1y
)])
881 indices
.extend([(id_last
, id_last
+1, id_last
+3),
882 (id_last
+3, id_last
+1, id_last
+2)])
886 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
887 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
888 m1x
= min(m1x
, area_width
)
889 m2x
= min(m2x
, area_width
)
890 if m1y
> bottom_bar
and m2y
> bottom_bar
:
891 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
892 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
893 indices
.extend([(id_last
, id_last
+1, id_last
+3),
894 (id_last
+3, id_last
+1, id_last
+2)])
898 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
899 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
900 m1y
= max(m1y
, bottom_bar
)
901 m2y
= max(m2y
, bottom_bar
)
902 if m1x
< area_width
and m2x
< area_width
:
903 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
904 (m1x
+radius
,m1y
), (m1x
,m1y
)])
905 indices
.extend([(id_last
, id_last
+1, id_last
+3),
906 (id_last
+3, id_last
+1, id_last
+2)])
910 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
911 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
912 m1x
= min(m1x
, area_width
)
913 m2x
= min(m2x
, area_width
)
914 if m1y
> bottom_bar
and m2y
> bottom_bar
:
915 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
916 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
917 indices
.extend([(id_last
, id_last
+1, id_last
+3),
918 (id_last
+3, id_last
+1, id_last
+2)])
920 # now draw all edges in one batch
921 if len(vertices
) != 0:
922 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
924 shader
.uniform_float("color", colour
)
927 def draw_callback_nodeoutline(self
, context
, mode
):
931 bgl
.glEnable(bgl
.GL_BLEND
)
932 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
933 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
935 nodes
, links
= get_nodes_links(context
)
937 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
940 col_outer
= (1.0, 0.2, 0.2, 0.4)
941 col_inner
= (0.0, 0.0, 0.0, 0.5)
942 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
943 elif mode
== "LINKMENU":
944 col_outer
= (0.4, 0.6, 1.0, 0.4)
945 col_inner
= (0.0, 0.0, 0.0, 0.5)
946 col_circle_inner
= (0.08, 0.15, .3, 1.0)
948 col_outer
= (0.2, 1.0, 0.2, 0.4)
949 col_inner
= (0.0, 0.0, 0.0, 0.5)
950 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
952 m1x
= self
.mouse_path
[0][0]
953 m1y
= self
.mouse_path
[0][1]
954 m2x
= self
.mouse_path
[-1][0]
955 m2y
= self
.mouse_path
[-1][1]
957 n1
= nodes
[context
.scene
.NWLazySource
]
958 n2
= nodes
[context
.scene
.NWLazyTarget
]
961 col_outer
= (0.4, 0.4, 0.4, 0.4)
962 col_inner
= (0.0, 0.0, 0.0, 0.5)
963 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
965 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
966 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
967 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
968 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
970 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
971 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
974 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
975 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
978 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
979 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
981 bgl
.glDisable(bgl
.GL_BLEND
)
982 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
984 def get_nodes_links(context
):
985 tree
= context
.space_data
.node_tree
987 # Get nodes from currently edited tree.
988 # If user is editing a group, space_data.node_tree is still the base level (outside group).
989 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
990 # the same as context.active_node, the user is in a group.
991 # Check recursively until we find the real active node_tree:
992 if tree
.nodes
.active
:
993 while tree
.nodes
.active
!= context
.active_node
:
994 tree
= tree
.nodes
.active
.node_tree
996 return tree
.nodes
, tree
.links
999 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
1000 base_color
: StringProperty(
1002 default
='diffuse diff albedo base col color',
1003 description
='Naming Components for Base Color maps')
1004 sss_color
: StringProperty(
1005 name
='Subsurface Color',
1006 default
='sss subsurface',
1007 description
='Naming Components for Subsurface Color maps')
1008 metallic
: StringProperty(
1010 default
='metallic metalness metal mtl',
1011 description
='Naming Components for metallness maps')
1012 specular
: StringProperty(
1014 default
='specularity specular spec spc',
1015 description
='Naming Components for Specular maps')
1016 normal
: StringProperty(
1018 default
='normal nor nrm nrml norm',
1019 description
='Naming Components for Normal maps')
1020 bump
: StringProperty(
1023 description
='Naming Components for bump maps')
1024 rough
: StringProperty(
1026 default
='roughness rough rgh',
1027 description
='Naming Components for roughness maps')
1028 gloss
: StringProperty(
1030 default
='gloss glossy glossiness',
1031 description
='Naming Components for glossy maps')
1032 displacement
: StringProperty(
1033 name
='Displacement',
1034 default
='displacement displace disp dsp height heightmap',
1035 description
='Naming Components for displacement maps')
1038 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
1039 bl_idname
= __name__
1041 merge_hide
: EnumProperty(
1042 name
="Hide Mix nodes",
1044 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1045 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1046 ("NEVER", "Never", "Never collapse the new merge nodes")
1048 default
='NON_SHADER',
1049 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1050 merge_position
: EnumProperty(
1051 name
="Mix Node Position",
1053 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1054 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1057 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1059 show_hotkey_list
: BoolProperty(
1060 name
="Show Hotkey List",
1062 description
="Expand this box into a list of all the hotkeys for functions in this addon"
1064 hotkey_list_filter
: StringProperty(
1065 name
=" Filter by Name",
1067 description
="Show only hotkeys that have this text in their name"
1069 show_principled_lists
: BoolProperty(
1070 name
="Show Principled naming tags",
1072 description
="Expand this box into a list of all naming tags for principled texture setup"
1074 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
1076 def draw(self
, context
):
1077 layout
= self
.layout
1078 col
= layout
.column()
1079 col
.prop(self
, "merge_position")
1080 col
.prop(self
, "merge_hide")
1083 col
= box
.column(align
=True)
1084 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
1085 if self
.show_principled_lists
:
1086 tags
= self
.principled_tags
1088 col
.prop(tags
, "base_color")
1089 col
.prop(tags
, "sss_color")
1090 col
.prop(tags
, "metallic")
1091 col
.prop(tags
, "specular")
1092 col
.prop(tags
, "rough")
1093 col
.prop(tags
, "gloss")
1094 col
.prop(tags
, "normal")
1095 col
.prop(tags
, "bump")
1096 col
.prop(tags
, "displacement")
1099 col
= box
.column(align
=True)
1100 hotkey_button_name
= "Show Hotkey List"
1101 if self
.show_hotkey_list
:
1102 hotkey_button_name
= "Hide Hotkey List"
1103 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
1104 if self
.show_hotkey_list
:
1105 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
1107 for hotkey
in kmi_defs
:
1109 hotkey_name
= hotkey
[7]
1111 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
1112 row
= col
.row(align
=True)
1113 row
.label(text
=hotkey_name
)
1114 keystr
= nice_hotkey_name(hotkey
[1])
1116 keystr
= "Shift " + keystr
1118 keystr
= "Alt " + keystr
1120 keystr
= "Ctrl " + keystr
1121 row
.label(text
=keystr
)
1125 def nw_check(context
):
1126 space
= context
.space_data
1127 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree"]
1130 if space
.type == 'NODE_EDITOR' and space
.node_tree
is not None and space
.tree_type
in valid_trees
:
1137 def poll(cls
, context
):
1138 return nw_check(context
)
1142 class NWLazyMix(Operator
, NWBase
):
1143 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1144 bl_idname
= "node.nw_lazy_mix"
1145 bl_label
= "Mix Nodes"
1146 bl_options
= {'REGISTER', 'UNDO'}
1148 def modal(self
, context
, event
):
1149 context
.area
.tag_redraw()
1150 nodes
, links
= get_nodes_links(context
)
1153 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1156 if not context
.scene
.NWBusyDrawing
:
1157 node1
= node_at_pos(nodes
, context
, event
)
1159 context
.scene
.NWBusyDrawing
= node1
.name
1161 if context
.scene
.NWBusyDrawing
!= 'STOP':
1162 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1164 context
.scene
.NWLazySource
= node1
.name
1165 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1167 if event
.type == 'MOUSEMOVE':
1168 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1170 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1171 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1172 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1175 node2
= node_at_pos(nodes
, context
, event
)
1177 context
.scene
.NWBusyDrawing
= node2
.name
1189 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
1191 context
.scene
.NWBusyDrawing
= ""
1194 elif event
.type == 'ESC':
1196 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1197 return {'CANCELLED'}
1199 return {'RUNNING_MODAL'}
1201 def invoke(self
, context
, event
):
1202 if context
.area
.type == 'NODE_EDITOR':
1203 # the arguments we pass the the callback
1204 args
= (self
, context
, 'MIX')
1205 # Add the region OpenGL drawing callback
1206 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1207 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1209 self
.mouse_path
= []
1211 context
.window_manager
.modal_handler_add(self
)
1212 return {'RUNNING_MODAL'}
1214 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1215 return {'CANCELLED'}
1218 class NWLazyConnect(Operator
, NWBase
):
1219 """Connect two nodes without clicking a specific socket (automatically determined"""
1220 bl_idname
= "node.nw_lazy_connect"
1221 bl_label
= "Lazy Connect"
1222 bl_options
= {'REGISTER', 'UNDO'}
1223 with_menu
: BoolProperty()
1225 def modal(self
, context
, event
):
1226 context
.area
.tag_redraw()
1227 nodes
, links
= get_nodes_links(context
)
1230 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1233 if not context
.scene
.NWBusyDrawing
:
1234 node1
= node_at_pos(nodes
, context
, event
)
1236 context
.scene
.NWBusyDrawing
= node1
.name
1238 if context
.scene
.NWBusyDrawing
!= 'STOP':
1239 node1
= nodes
[context
.scene
.NWBusyDrawing
]
1241 context
.scene
.NWLazySource
= node1
.name
1242 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
1244 if event
.type == 'MOUSEMOVE':
1245 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
1247 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
1248 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
1249 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1252 node2
= node_at_pos(nodes
, context
, event
)
1254 context
.scene
.NWBusyDrawing
= node2
.name
1259 link_success
= False
1265 if node
.select
== True:
1267 original_sel
.append(node
)
1269 original_unsel
.append(node
)
1273 #link_success = autolink(node1, node2, links)
1275 if len(node1
.outputs
) > 1 and node2
.inputs
:
1276 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
1277 elif len(node1
.outputs
) == 1:
1278 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
1280 link_success
= autolink(node1
, node2
, links
)
1282 for node
in original_sel
:
1284 for node
in original_unsel
:
1288 force_update(context
)
1289 context
.scene
.NWBusyDrawing
= ""
1292 elif event
.type == 'ESC':
1293 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1294 return {'CANCELLED'}
1296 return {'RUNNING_MODAL'}
1298 def invoke(self
, context
, event
):
1299 if context
.area
.type == 'NODE_EDITOR':
1300 nodes
, links
= get_nodes_links(context
)
1301 node
= node_at_pos(nodes
, context
, event
)
1303 context
.scene
.NWBusyDrawing
= node
.name
1305 # the arguments we pass the the callback
1309 args
= (self
, context
, mode
)
1310 # Add the region OpenGL drawing callback
1311 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1312 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1314 self
.mouse_path
= []
1316 context
.window_manager
.modal_handler_add(self
)
1317 return {'RUNNING_MODAL'}
1319 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1320 return {'CANCELLED'}
1323 class NWDeleteUnused(Operator
, NWBase
):
1324 """Delete all nodes whose output is not used"""
1325 bl_idname
= 'node.nw_del_unused'
1326 bl_label
= 'Delete Unused Nodes'
1327 bl_options
= {'REGISTER', 'UNDO'}
1329 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1330 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1332 def is_unused_node(self
, node
):
1333 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1334 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1335 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1336 if node
.type in end_types
:
1339 for output
in node
.outputs
:
1345 def poll(cls
, context
):
1347 if nw_check(context
):
1348 if context
.space_data
.node_tree
.nodes
:
1352 def execute(self
, context
):
1353 nodes
, links
= get_nodes_links(context
)
1358 if node
.select
== True:
1359 selection
.append(node
.name
)
1365 temp_deleted_nodes
= []
1366 del_unused_iterations
= len(nodes
)
1367 for it
in range(0, del_unused_iterations
):
1368 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1370 if self
.is_unused_node(node
):
1372 deleted_nodes
.append(node
.name
)
1373 bpy
.ops
.node
.delete()
1375 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1378 if self
.delete_frames
:
1386 frames_in_use
.append(node
.parent
)
1388 if node
.type == 'FRAME' and node
not in frames_in_use
:
1391 repeat
= True # repeat for nested frames
1393 if node
not in frames_in_use
:
1395 deleted_nodes
.append(node
.name
)
1396 bpy
.ops
.node
.delete()
1398 if self
.delete_muted
:
1402 deleted_nodes
.append(node
.name
)
1403 bpy
.ops
.node
.delete_reconnect()
1405 # get unique list of deleted nodes (iterations would count the same node more than once)
1406 deleted_nodes
= list(set(deleted_nodes
))
1407 for n
in deleted_nodes
:
1408 self
.report({'INFO'}, "Node " + n
+ " deleted")
1409 num_deleted
= len(deleted_nodes
)
1414 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1416 self
.report({'INFO'}, "Nothing deleted")
1419 nodes
, links
= get_nodes_links(context
)
1421 if node
.name
in selection
:
1425 def invoke(self
, context
, event
):
1426 return context
.window_manager
.invoke_confirm(self
, event
)
1429 class NWSwapLinks(Operator
, NWBase
):
1430 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1431 bl_idname
= 'node.nw_swap_links'
1432 bl_label
= 'Swap Links'
1433 bl_options
= {'REGISTER', 'UNDO'}
1436 def poll(cls
, context
):
1438 if nw_check(context
):
1439 if context
.selected_nodes
:
1440 valid
= len(context
.selected_nodes
) <= 2
1443 def execute(self
, context
):
1444 nodes
, links
= get_nodes_links(context
)
1445 selected_nodes
= context
.selected_nodes
1446 n1
= selected_nodes
[0]
1449 if len(selected_nodes
) == 2:
1450 n2
= selected_nodes
[1]
1451 if n1
.outputs
and n2
.outputs
:
1456 for output
in n1
.outputs
:
1458 for link
in output
.links
:
1459 n1_outputs
.append([out_index
, link
.to_socket
])
1464 for output
in n2
.outputs
:
1466 for link
in output
.links
:
1467 n2_outputs
.append([out_index
, link
.to_socket
])
1471 for connection
in n1_outputs
:
1473 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1475 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1476 for connection
in n2_outputs
:
1478 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1480 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1482 if n1
.outputs
or n2
.outputs
:
1483 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1485 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1488 elif len(selected_nodes
) == 1:
1492 for i1
in n1
.inputs
:
1495 for i2
in n1
.inputs
:
1496 if i1
.type == i2
.type and i2
.is_linked
:
1498 types
.append ([i1
, similar_types
, i
])
1500 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1505 for i2
in n1
.inputs
:
1506 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1508 i1f
= pair
[0].links
[0].from_socket
1509 i1t
= pair
[0].links
[0].to_socket
1510 i2f
= pair
[1].links
[0].from_socket
1511 i2t
= pair
[1].links
[0].to_socket
1516 fs
= t
[0].links
[0].from_socket
1518 links
.remove(t
[0].links
[0])
1519 if i
+1 == len(n1
.inputs
):
1522 while n1
.inputs
[i
].is_linked
:
1524 links
.new(fs
, n1
.inputs
[i
])
1525 elif len(types
) == 2:
1526 i1f
= types
[0][0].links
[0].from_socket
1527 i1t
= types
[0][0].links
[0].to_socket
1528 i2f
= types
[1][0].links
[0].from_socket
1529 i2t
= types
[1][0].links
[0].to_socket
1534 self
.report({'WARNING'}, "This node has no input connections to swap!")
1536 self
.report({'WARNING'}, "This node has no inputs to swap!")
1538 force_update(context
)
1542 class NWResetBG(Operator
, NWBase
):
1543 """Reset the zoom and position of the background image"""
1544 bl_idname
= 'node.nw_bg_reset'
1545 bl_label
= 'Reset Backdrop'
1546 bl_options
= {'REGISTER', 'UNDO'}
1549 def poll(cls
, context
):
1551 if nw_check(context
):
1552 snode
= context
.space_data
1553 valid
= snode
.tree_type
== 'CompositorNodeTree'
1556 def execute(self
, context
):
1557 context
.space_data
.backdrop_zoom
= 1
1558 context
.space_data
.backdrop_offset
[0] = 0
1559 context
.space_data
.backdrop_offset
[1] = 0
1563 class NWAddAttrNode(Operator
, NWBase
):
1564 """Add an Attribute node with this name"""
1565 bl_idname
= 'node.nw_add_attr_node'
1566 bl_label
= 'Add UV map'
1567 bl_options
= {'REGISTER', 'UNDO'}
1569 attr_name
: StringProperty()
1571 def execute(self
, context
):
1572 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1573 nodes
, links
= get_nodes_links(context
)
1574 nodes
.active
.attribute_name
= self
.attr_name
1578 class NWEmissionViewer(Operator
, NWBase
):
1579 bl_idname
= "node.nw_emission_viewer"
1580 bl_label
= "Emission Viewer"
1581 bl_description
= "Connect active node to Emission Shader for shadeless previews"
1582 bl_options
= {'REGISTER', 'UNDO'}
1585 def poll(cls
, context
):
1586 is_cycles
= is_cycles_or_eevee(context
)
1587 if nw_check(context
):
1588 space
= context
.space_data
1589 if space
.tree_type
== 'ShaderNodeTree' and is_cycles
:
1590 if context
.active_node
:
1591 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1597 def invoke(self
, context
, event
):
1598 space
= context
.space_data
1599 shader_type
= space
.shader_type
1600 if shader_type
== 'OBJECT':
1601 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1602 shader_output_type
= "OUTPUT_MATERIAL"
1603 shader_output_ident
= "ShaderNodeOutputMaterial"
1604 shader_viewer_ident
= "ShaderNodeEmission"
1606 shader_output_type
= "OUTPUT_LIGHT"
1607 shader_output_ident
= "ShaderNodeOutputLight"
1608 shader_viewer_ident
= "ShaderNodeEmission"
1610 elif shader_type
== 'WORLD':
1611 shader_output_type
= "OUTPUT_WORLD"
1612 shader_output_ident
= "ShaderNodeOutputWorld"
1613 shader_viewer_ident
= "ShaderNodeBackground"
1614 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
]
1615 mlocx
= event
.mouse_region_x
1616 mlocy
= event
.mouse_region_y
1617 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
1618 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1619 nodes
, links
= get_nodes_links(context
)
1620 in_group
= context
.active_node
!= space
.node_tree
.nodes
.active
1621 active
= nodes
.active
1622 output_types
= [x
[1] for x
in shaders_output_nodes_props
]
1625 if (active
.name
!= "Emission Viewer") and (active
.type not in output_types
) and not in_group
:
1626 for out
in active
.outputs
:
1627 if is_visible_socket(out
):
1631 # get material_output node, store selection, deselect all
1632 materialout
= None # placeholder node
1635 if node
.type == shader_output_type
:
1638 selection
.append(node
.name
)
1641 # get right-most location
1642 sorted_by_xloc
= (sorted(nodes
, key
=lambda x
: x
.location
.x
))
1643 max_xloc_node
= sorted_by_xloc
[-1]
1644 if max_xloc_node
.name
== 'Emission Viewer':
1645 max_xloc_node
= sorted_by_xloc
[-2]
1647 # get average y location
1650 sum_yloc
+= node
.location
.y
1652 new_locx
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
1653 new_locy
= sum_yloc
/ len(nodes
)
1655 materialout
= nodes
.new(shader_output_ident
)
1656 materialout
.location
.x
= new_locx
1657 materialout
.location
.y
= new_locy
1658 materialout
.select
= False
1659 # Analyze outputs, add "Emission Viewer" if needed, make links
1662 for i
, out
in enumerate(active
.outputs
):
1663 if is_visible_socket(out
):
1664 valid_outputs
.append(i
)
1666 out_i
= valid_outputs
[0] # Start index of node's outputs
1667 for i
, valid_i
in enumerate(valid_outputs
):
1668 for out_link
in active
.outputs
[valid_i
].links
:
1669 if "Emission Viewer" in out_link
.to_node
.name
or (out_link
.to_node
== materialout
and out_link
.to_socket
== materialout
.inputs
[0]):
1670 if i
< len(valid_outputs
) - 1:
1671 out_i
= valid_outputs
[i
+ 1]
1673 out_i
= valid_outputs
[0]
1674 make_links
= [] # store sockets for new links
1676 # If output type not 'SHADER' - "Emission Viewer" needed
1677 if active
.outputs
[out_i
].type != 'SHADER':
1678 # get Emission Viewer node
1679 emission_exists
= False
1680 emission_placeholder
= nodes
[0]
1682 if "Emission Viewer" in node
.name
:
1683 emission_exists
= True
1684 emission_placeholder
= node
1685 if not emission_exists
:
1686 emission
= nodes
.new(shader_viewer_ident
)
1687 emission
.hide
= True
1688 emission
.location
= [materialout
.location
.x
, (materialout
.location
.y
+ 40)]
1689 emission
.label
= "Viewer"
1690 emission
.name
= "Emission Viewer"
1691 emission
.use_custom_color
= True
1692 emission
.color
= (0.6, 0.5, 0.4)
1693 emission
.select
= False
1695 emission
= emission_placeholder
1696 make_links
.append((active
.outputs
[out_i
], emission
.inputs
[0]))
1698 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
1699 if emission
.outputs
[0].links
.__len
__() > 0:
1700 if not emission
.outputs
[0].links
[0].to_node
== materialout
:
1701 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
1703 make_links
.append((emission
.outputs
[0], materialout
.inputs
[0]))
1705 # Set brightness of viewer to compensate for Film and CM exposure
1706 if context
.scene
.render
.engine
== 'CYCLES' and hasattr(context
.scene
, 'cycles'):
1707 intensity
= 1/context
.scene
.cycles
.film_exposure
# Film exposure is a multiplier
1711 intensity
/= pow(2, (context
.scene
.view_settings
.exposure
)) # CM exposure is measured in stops/EVs (2^x)
1712 emission
.inputs
[1].default_value
= intensity
1715 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
1716 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[1 if active
.outputs
[out_i
].name
== "Volume" else 0]))
1718 if node
.name
== 'Emission Viewer':
1720 bpy
.ops
.node
.delete()
1721 for li_from
, li_to
in make_links
:
1722 links
.new(li_from
, li_to
)
1724 nodes
.active
= active
1726 if node
.name
in selection
:
1728 force_update(context
)
1731 return {'CANCELLED'}
1734 class NWFrameSelected(Operator
, NWBase
):
1735 bl_idname
= "node.nw_frame_selected"
1736 bl_label
= "Frame Selected"
1737 bl_description
= "Add a frame node and parent the selected nodes to it"
1738 bl_options
= {'REGISTER', 'UNDO'}
1740 label_prop
: StringProperty(
1742 description
='The visual name of the frame node',
1745 color_prop
: FloatVectorProperty(
1747 description
="The color of the frame node",
1748 default
=(0.6, 0.6, 0.6),
1749 min=0, max=1, step
=1, precision
=3,
1750 subtype
='COLOR_GAMMA', size
=3
1753 def execute(self
, context
):
1754 nodes
, links
= get_nodes_links(context
)
1757 if node
.select
== True:
1758 selected
.append(node
)
1760 bpy
.ops
.node
.add_node(type='NodeFrame')
1762 frm
.label
= self
.label_prop
1763 frm
.use_custom_color
= True
1764 frm
.color
= self
.color_prop
1766 for node
in selected
:
1772 class NWReloadImages(Operator
, NWBase
):
1773 bl_idname
= "node.nw_reload_images"
1774 bl_label
= "Reload Images"
1775 bl_description
= "Update all the image nodes to match their files on disk"
1777 def execute(self
, context
):
1778 nodes
, links
= get_nodes_links(context
)
1779 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1782 if node
.type in image_types
:
1783 if node
.type == "TEXTURE":
1784 if node
.texture
: # node has texture assigned
1785 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1786 if node
.texture
.image
: # texture has image assigned
1787 node
.texture
.image
.reload()
1795 self
.report({'INFO'}, "Reloaded images")
1796 print("Reloaded " + str(num_reloaded
) + " images")
1797 force_update(context
)
1800 self
.report({'WARNING'}, "No images found to reload in this node tree")
1801 return {'CANCELLED'}
1804 class NWSwitchNodeType(Operator
, NWBase
):
1805 """Switch type of selected nodes """
1806 bl_idname
= "node.nw_swtch_node_type"
1807 bl_label
= "Switch Node Type"
1808 bl_options
= {'REGISTER', 'UNDO'}
1810 to_type
: EnumProperty(
1811 name
="Switch to type",
1812 items
=list(shaders_input_nodes_props
) +
1813 list(shaders_output_nodes_props
) +
1814 list(shaders_shader_nodes_props
) +
1815 list(shaders_texture_nodes_props
) +
1816 list(shaders_color_nodes_props
) +
1817 list(shaders_vector_nodes_props
) +
1818 list(shaders_converter_nodes_props
) +
1819 list(shaders_layout_nodes_props
) +
1820 list(compo_input_nodes_props
) +
1821 list(compo_output_nodes_props
) +
1822 list(compo_color_nodes_props
) +
1823 list(compo_converter_nodes_props
) +
1824 list(compo_filter_nodes_props
) +
1825 list(compo_vector_nodes_props
) +
1826 list(compo_matte_nodes_props
) +
1827 list(compo_distort_nodes_props
) +
1828 list(compo_layout_nodes_props
) +
1829 list(blender_mat_input_nodes_props
) +
1830 list(blender_mat_output_nodes_props
) +
1831 list(blender_mat_color_nodes_props
) +
1832 list(blender_mat_vector_nodes_props
) +
1833 list(blender_mat_converter_nodes_props
) +
1834 list(blender_mat_layout_nodes_props
) +
1835 list(texture_input_nodes_props
) +
1836 list(texture_output_nodes_props
) +
1837 list(texture_color_nodes_props
) +
1838 list(texture_pattern_nodes_props
) +
1839 list(texture_textures_nodes_props
) +
1840 list(texture_converter_nodes_props
) +
1841 list(texture_distort_nodes_props
) +
1842 list(texture_layout_nodes_props
)
1845 def execute(self
, context
):
1846 nodes
, links
= get_nodes_links(context
)
1847 to_type
= self
.to_type
1848 # Those types of nodes will not swap.
1849 src_excludes
= ('NodeFrame')
1850 # Those attributes of nodes will be copied if possible
1851 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
1852 'show_options', 'show_preview', 'show_texture',
1853 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1855 selected
= [n
for n
in nodes
if n
.select
]
1857 for node
in [n
for n
in selected
if
1858 n
.rna_type
.identifier
not in src_excludes
and
1859 n
.rna_type
.identifier
!= to_type
]:
1860 new_node
= nodes
.new(to_type
)
1861 for attr
in attrs_to_pass
:
1862 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
1863 setattr(new_node
, attr
, getattr(node
, attr
))
1864 # set image datablock of dst to image of src
1865 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
1867 new_node
.image
= node
.image
1869 if new_node
.type == 'SWITCH':
1870 new_node
.hide
= True
1871 # Dictionaries: src_sockets and dst_sockets:
1872 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1873 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1874 # in 'INPUTS' and 'OUTPUTS':
1875 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1877 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1879 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1880 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1883 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1884 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1886 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1887 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1888 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1889 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
1890 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1891 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
1892 # enumerate in inputs, then in outputs
1893 # find name, default value and links of socket
1894 for i
, socket
in enumerate(in_out
):
1895 the_name
= socket
.name
1897 # Not every socket, especially in outputs has "default_value"
1898 if hasattr(socket
, 'default_value'):
1899 dval
= socket
.default_value
1901 for lnk
in socket
.links
:
1902 socket_links
.append(lnk
)
1903 # check type of socket to fill proper keys.
1904 for the_type
in types_order_one
:
1905 if socket
.type == the_type
:
1906 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1907 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1908 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
1909 # Check which of the types in inputs/outputs is considered to be "main".
1910 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1911 for type_check
in types_order_one
:
1912 if sockets
[in_out_name
][type_check
]:
1913 sockets
[in_out_name
]['MAIN'] = type_check
1917 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1918 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1921 for inout
, soctype
in (
1922 ('INPUTS', 'MAIN',),
1923 ('INPUTS', 'SHADER',),
1924 ('INPUTS', 'RGBA',),
1925 ('INPUTS', 'VECTOR',),
1926 ('INPUTS', 'VALUE',),
1927 ('OUTPUTS', 'MAIN',),
1928 ('OUTPUTS', 'SHADER',),
1929 ('OUTPUTS', 'RGBA',),
1930 ('OUTPUTS', 'VECTOR',),
1931 ('OUTPUTS', 'VALUE',),
1933 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
1934 if soctype
== 'MAIN':
1935 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
1936 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
1938 sc
= src_sockets
[inout
][soctype
]
1939 dt
= dst_sockets
[inout
][soctype
]
1940 # start with 'dt' to determine number of possibilities.
1941 for i
, soc
in enumerate(dt
):
1942 # if src main has enough entries - match them with dst main sockets by indexes.
1944 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
1945 # add 'VALUE_NAME' criterion to inputs.
1946 if inout
== 'INPUTS' and soctype
== 'VALUE':
1948 if s
[2] == soc
[2]: # if names match
1949 # append src (index, dval), dst (index, dval)
1950 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
1952 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1953 # This creates better links when relinking textures.
1954 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
1955 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
1957 # Pass default values and RELINK:
1958 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1959 # INPUTS: Base on matches in proper order.
1960 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
1962 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
1963 new_node
.inputs
[dst_i
].default_value
= src_dval
1964 # Special case: switch to math
1965 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1966 new_node
.type == 'MATH' and\
1968 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
1969 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
1970 if node
.type == 'MIX_RGB':
1971 if node
.blend_type
in [o
[0] for o
in operations
]:
1972 new_node
.operation
= node
.blend_type
1973 # Special case: switch from math to some types
1974 if node
.type == 'MATH' and\
1975 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1978 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
1979 if new_node
.type == 'MIX_RGB':
1980 if node
.operation
in [t
[0] for t
in blend_types
]:
1981 new_node
.blend_type
= node
.operation
1982 # Set Fac of MIX_RGB to 1.0
1983 new_node
.inputs
[0].default_value
= 1.0
1984 # make link only when dst matching input is not linked already.
1985 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
1986 in_src_link
= node
.inputs
[src_i
].links
[0]
1987 in_dst_socket
= new_node
.inputs
[dst_i
]
1988 links
.new(in_src_link
.from_socket
, in_dst_socket
)
1989 links
.remove(in_src_link
)
1990 # OUTPUTS: Base on matches in proper order.
1991 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
1992 for out_src_link
in node
.outputs
[src_i
].links
:
1993 out_dst_socket
= new_node
.outputs
[dst_i
]
1994 links
.new(out_dst_socket
, out_src_link
.to_socket
)
1995 # relink rest inputs if possible, no criteria
1996 for src_inp
in node
.inputs
:
1997 for dst_inp
in new_node
.inputs
:
1998 if src_inp
.links
and not dst_inp
.links
:
1999 src_link
= src_inp
.links
[0]
2000 links
.new(src_link
.from_socket
, dst_inp
)
2001 links
.remove(src_link
)
2002 # relink rest outputs if possible, base on node kind if any left.
2003 for src_o
in node
.outputs
:
2004 for out_src_link
in src_o
.links
:
2005 for dst_o
in new_node
.outputs
:
2006 if src_o
.type == dst_o
.type:
2007 links
.new(dst_o
, out_src_link
.to_socket
)
2008 # relink rest outputs no criteria if any left. Link all from first output.
2009 for src_o
in node
.outputs
:
2010 for out_src_link
in src_o
.links
:
2011 if new_node
.outputs
:
2012 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
2014 force_update(context
)
2018 class NWMergeNodes(Operator
, NWBase
):
2019 bl_idname
= "node.nw_merge_nodes"
2020 bl_label
= "Merge Nodes"
2021 bl_description
= "Merge Selected Nodes"
2022 bl_options
= {'REGISTER', 'UNDO'}
2026 description
="All possible blend types and math operations",
2027 items
=blend_types
+ [op
for op
in operations
if op
not in blend_types
],
2029 merge_type
: EnumProperty(
2031 description
="Type of Merge to be used",
2033 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2034 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2035 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2036 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2037 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2038 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2042 def execute(self
, context
):
2043 settings
= context
.preferences
.addons
[__name__
].preferences
2044 merge_hide
= settings
.merge_hide
2045 merge_position
= settings
.merge_position
# 'center' or 'bottom'
2048 do_hide_shader
= False
2049 if merge_hide
== 'ALWAYS':
2051 do_hide_shader
= True
2052 elif merge_hide
== 'NON_SHADER':
2055 tree_type
= context
.space_data
.node_tree
.type
2056 if tree_type
== 'COMPOSITING':
2057 node_type
= 'CompositorNode'
2058 elif tree_type
== 'SHADER':
2059 node_type
= 'ShaderNode'
2060 elif tree_type
== 'TEXTURE':
2061 node_type
= 'TextureNode'
2062 nodes
, links
= get_nodes_links(context
)
2064 merge_type
= self
.merge_type
2065 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2066 # 'ZCOMBINE' works only if mode == 'MIX'
2067 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2068 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2071 selected_mix
= [] # entry = [index, loc]
2072 selected_shader
= [] # entry = [index, loc]
2073 selected_math
= [] # entry = [index, loc]
2074 selected_z
= [] # entry = [index, loc]
2075 selected_alphaover
= [] # entry = [index, loc]
2077 for i
, node
in enumerate(nodes
):
2078 if node
.select
and node
.outputs
:
2079 if merge_type
== 'AUTO':
2080 for (type, types_list
, dst
) in (
2081 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2082 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2083 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2085 output_type
= node
.outputs
[0].type
2086 valid_mode
= mode
in types_list
2087 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2088 # Cheat that output type is 'RGBA',
2089 # and that 'MIX' exists in math operations list.
2090 # This way when selected_mix list is analyzed:
2091 # Node data will be appended even though it doesn't meet requirements.
2092 if output_type
!= 'SHADER' and mode
== 'MIX':
2093 output_type
= 'RGBA'
2095 if output_type
== type and valid_mode
:
2096 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2098 for (type, types_list
, dst
) in (
2099 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2100 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2101 ('MATH', [t
[0] for t
in operations
], selected_math
),
2102 ('ZCOMBINE', ('MIX', ), selected_z
),
2103 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2105 if merge_type
== type and mode
in types_list
:
2106 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2107 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2108 # use only 'Mix' nodes for merging.
2109 # For that we add selected_math list to selected_mix list and clear selected_math.
2110 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2111 selected_mix
+= selected_math
2114 for nodes_list
in [selected_mix
, selected_shader
, selected_math
, selected_z
, selected_alphaover
]:
2116 count_before
= len(nodes
)
2117 # sort list by loc_x - reversed
2118 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2120 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2121 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2122 if merge_position
== 'CENTER':
2123 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)
2124 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2130 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2134 if nodes_list
== selected_shader
and not do_hide_shader
:
2136 the_range
= len(nodes_list
) - 1
2137 if len(nodes_list
) == 1:
2139 for i
in range(the_range
):
2140 if nodes_list
== selected_mix
:
2141 add_type
= node_type
+ 'MixRGB'
2142 add
= nodes
.new(add_type
)
2143 add
.blend_type
= mode
2145 add
.inputs
[0].default_value
= 1.0
2146 add
.show_preview
= False
2152 add
.width_hidden
= 100.0
2153 elif nodes_list
== selected_math
:
2154 add_type
= node_type
+ 'Math'
2155 add
= nodes
.new(add_type
)
2156 add
.operation
= mode
2162 add
.width_hidden
= 100.0
2163 elif nodes_list
== selected_shader
:
2165 add_type
= node_type
+ 'MixShader'
2166 add
= nodes
.new(add_type
)
2167 add
.hide
= do_hide_shader
2172 add
.width_hidden
= 100.0
2174 add_type
= node_type
+ 'AddShader'
2175 add
= nodes
.new(add_type
)
2176 add
.hide
= do_hide_shader
2181 add
.width_hidden
= 100.0
2182 elif nodes_list
== selected_z
:
2183 add
= nodes
.new('CompositorNodeZcombine')
2184 add
.show_preview
= False
2190 add
.width_hidden
= 100.0
2191 elif nodes_list
== selected_alphaover
:
2192 add
= nodes
.new('CompositorNodeAlphaOver')
2193 add
.show_preview
= False
2199 add
.width_hidden
= 100.0
2200 add
.location
= loc_x
, loc_y
2204 count_after
= len(nodes
)
2205 index
= count_after
- 1
2206 first_selected
= nodes
[nodes_list
[0][0]]
2207 # "last" node has been added as first, so its index is count_before.
2208 last_add
= nodes
[count_before
]
2210 # Two nodes were selected and first selected has no output links, second selected has output links.
2211 # Then add links from last add to all links 'to_socket' of out links of second selected.
2212 if len(nodes_list
) == 2:
2213 if not first_selected
.outputs
[0].links
:
2214 second_selected
= nodes
[nodes_list
[1][0]]
2215 for ss_link
in second_selected
.outputs
[0].links
:
2216 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2217 # Create list of invalid indexes.
2218 invalid_i
= [n
[0] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
)]
2219 # Link only if "to_node" index not in invalid indexes list.
2220 if ss_link
.to_node
not in [nodes
[i
] for i
in invalid_i
]:
2221 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2222 # add links from last_add to all links 'to_socket' of out links of first selected.
2223 for fs_link
in first_selected
.outputs
[0].links
:
2224 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2225 # Create list of invalid indexes.
2226 invalid_i
= [n
[0] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
)]
2227 # Link only if "to_node" index not in invalid indexes list.
2228 if fs_link
.to_node
not in [nodes
[i
] for i
in invalid_i
]:
2229 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2230 # add link from "first" selected and "first" add node
2231 node_to
= nodes
[count_after
- 1]
2232 links
.new(first_selected
.outputs
[0], node_to
.inputs
[first
])
2233 if node_to
.type == 'ZCOMBINE':
2234 for fs_out
in first_selected
.outputs
:
2235 if fs_out
!= first_selected
.outputs
[0] and fs_out
.name
in ('Z', 'Depth'):
2236 links
.new(fs_out
, node_to
.inputs
[1])
2238 # add links between added ADD nodes and between selected and ADD nodes
2239 for i
in range(count_adds
):
2240 if i
< count_adds
- 1:
2241 node_from
= nodes
[index
]
2242 node_to
= nodes
[index
- 1]
2243 node_to_input_i
= first
2244 node_to_z_i
= 1 # if z combine - link z to first z input
2245 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2246 if node_to
.type == 'ZCOMBINE':
2247 for from_out
in node_from
.outputs
:
2248 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2249 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2250 if len(nodes_list
) > 1:
2251 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2252 node_to
= nodes
[index
]
2253 node_to_input_i
= second
2254 node_to_z_i
= 3 # if z combine - link z to second z input
2255 links
.new(node_from
.outputs
[0], node_to
.inputs
[node_to_input_i
])
2256 if node_to
.type == 'ZCOMBINE':
2257 for from_out
in node_from
.outputs
:
2258 if from_out
!= node_from
.outputs
[0] and from_out
.name
in ('Z', 'Depth'):
2259 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2261 # set "last" of added nodes as active
2262 nodes
.active
= last_add
2263 for i
, x
, y
, dx
, h
in nodes_list
:
2264 nodes
[i
].select
= False
2269 class NWBatchChangeNodes(Operator
, NWBase
):
2270 bl_idname
= "node.nw_batch_change"
2271 bl_label
= "Batch Change"
2272 bl_description
= "Batch Change Blend Type and Math Operation"
2273 bl_options
= {'REGISTER', 'UNDO'}
2275 blend_type
: EnumProperty(
2277 items
=blend_types
+ navs
,
2279 operation
: EnumProperty(
2281 items
=operations
+ navs
,
2284 def execute(self
, context
):
2286 nodes
, links
= get_nodes_links(context
)
2287 blend_type
= self
.blend_type
2288 operation
= self
.operation
2289 for node
in context
.selected_nodes
:
2290 if node
.type == 'MIX_RGB':
2291 if not blend_type
in [nav
[0] for nav
in navs
]:
2292 node
.blend_type
= blend_type
2294 if blend_type
== 'NEXT':
2295 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2296 #index = blend_types.index(node.blend_type)
2297 if index
== len(blend_types
) - 1:
2298 node
.blend_type
= blend_types
[0][0]
2300 node
.blend_type
= blend_types
[index
+ 1][0]
2302 if blend_type
== 'PREV':
2303 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2305 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2307 node
.blend_type
= blend_types
[index
- 1][0]
2309 if node
.type == 'MATH':
2310 if not operation
in [nav
[0] for nav
in navs
]:
2311 node
.operation
= operation
2313 if operation
== 'NEXT':
2314 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2315 #index = operations.index(node.operation)
2316 if index
== len(operations
) - 1:
2317 node
.operation
= operations
[0][0]
2319 node
.operation
= operations
[index
+ 1][0]
2321 if operation
== 'PREV':
2322 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2323 #index = operations.index(node.operation)
2325 node
.operation
= operations
[len(operations
) - 1][0]
2327 node
.operation
= operations
[index
- 1][0]
2332 class NWChangeMixFactor(Operator
, NWBase
):
2333 bl_idname
= "node.nw_factor"
2334 bl_label
= "Change Factor"
2335 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2336 bl_options
= {'REGISTER', 'UNDO'}
2338 # option: Change factor.
2339 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2340 # Else - change factor by option value.
2341 option
: FloatProperty()
2343 def execute(self
, context
):
2344 nodes
, links
= get_nodes_links(context
)
2345 option
= self
.option
2346 selected
= [] # entry = index
2347 for si
, node
in enumerate(nodes
):
2349 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2353 fac
= nodes
[si
].inputs
[0]
2354 nodes
[si
].hide
= False
2355 if option
in {0.0, 1.0}:
2356 fac
.default_value
= option
2358 fac
.default_value
+= option
2363 class NWCopySettings(Operator
, NWBase
):
2364 bl_idname
= "node.nw_copy_settings"
2365 bl_label
= "Copy Settings"
2366 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2367 bl_options
= {'REGISTER', 'UNDO'}
2370 def poll(cls
, context
):
2372 if nw_check(context
):
2374 context
.active_node
is not None and
2375 context
.active_node
.type != 'FRAME'
2380 def execute(self
, context
):
2381 node_active
= context
.active_node
2382 node_selected
= context
.selected_nodes
2385 if not (len(node_selected
) > 1):
2386 self
.report({'ERROR'}, "2 nodes must be selected at least")
2387 return {'CANCELLED'}
2389 # Check if active node is in the selection
2390 selected_node_names
= [n
.name
for n
in node_selected
]
2391 if node_active
.name
not in selected_node_names
:
2392 self
.report({'ERROR'}, "No active node")
2393 return {'CANCELLED'}
2395 # Get nodes in selection by type
2396 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2398 if not (len(valid_nodes
) > 1) and node_active
:
2399 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2400 return {'CANCELLED'}
2402 if len(valid_nodes
) != len(node_selected
):
2403 # Report nodes that are not valid
2404 valid_node_names
= [n
.name
for n
in valid_nodes
]
2405 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2406 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2408 # Reference original
2410 #node_selected_names = [n.name for n in node_selected]
2415 # Deselect all nodes
2416 for i
in node_selected
:
2419 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2420 # Run through all other nodes
2421 for node
in valid_nodes
[1:]:
2423 # Check for frame node
2424 parent
= node
.parent
if node
.parent
else None
2425 node_loc
= [node
.location
.x
, node
.location
.y
]
2427 # Select original to duplicate
2430 # Duplicate selected node
2431 bpy
.ops
.node
.duplicate()
2432 new_node
= context
.selected_nodes
[0]
2435 new_node
.select
= False
2437 # Properties to copy
2438 node_tree
= node
.id_data
2439 props_to_copy
= 'bl_idname name location height width'.split(' ')
2443 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2444 for i
in (i
for i
in mappings
if i
.is_linked
):
2446 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2449 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2450 props_to_copy
.pop(0)
2452 for prop
in props_to_copy
:
2453 setattr(new_node
, prop
, props
[prop
])
2455 # Get the node tree to remove the old node
2456 nodes
= node_tree
.nodes
2458 new_node
.name
= props
['name']
2461 new_node
.parent
= parent
2462 new_node
.location
= node_loc
2464 for str_from
, str_to
in reconnections
:
2465 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2467 success_names
.append(new_node
.name
)
2470 node_tree
.nodes
.active
= orig
2471 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2475 class NWCopyLabel(Operator
, NWBase
):
2476 bl_idname
= "node.nw_copy_label"
2477 bl_label
= "Copy Label"
2478 bl_options
= {'REGISTER', 'UNDO'}
2480 option
: EnumProperty(
2482 description
="Source of name of label",
2484 ('FROM_ACTIVE', 'from active', 'from active node',),
2485 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2486 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2490 def execute(self
, context
):
2491 nodes
, links
= get_nodes_links(context
)
2492 option
= self
.option
2493 active
= nodes
.active
2494 if option
== 'FROM_ACTIVE':
2496 src_label
= active
.label
2497 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2498 node
.label
= src_label
2499 elif option
== 'FROM_NODE':
2500 selected
= [n
for n
in nodes
if n
.select
]
2501 for node
in selected
:
2502 for input in node
.inputs
:
2504 src
= input.links
[0].from_node
2505 node
.label
= src
.label
2507 elif option
== 'FROM_SOCKET':
2508 selected
= [n
for n
in nodes
if n
.select
]
2509 for node
in selected
:
2510 for input in node
.inputs
:
2512 src
= input.links
[0].from_socket
2513 node
.label
= src
.name
2519 class NWClearLabel(Operator
, NWBase
):
2520 bl_idname
= "node.nw_clear_label"
2521 bl_label
= "Clear Label"
2522 bl_options
= {'REGISTER', 'UNDO'}
2524 option
: BoolProperty()
2526 def execute(self
, context
):
2527 nodes
, links
= get_nodes_links(context
)
2528 for node
in [n
for n
in nodes
if n
.select
]:
2533 def invoke(self
, context
, event
):
2535 return self
.execute(context
)
2537 return context
.window_manager
.invoke_confirm(self
, event
)
2540 class NWModifyLabels(Operator
, NWBase
):
2541 """Modify Labels of all selected nodes"""
2542 bl_idname
= "node.nw_modify_labels"
2543 bl_label
= "Modify Labels"
2544 bl_options
= {'REGISTER', 'UNDO'}
2546 prepend
: StringProperty(
2547 name
="Add to Beginning"
2549 append
: StringProperty(
2552 replace_from
: StringProperty(
2553 name
="Text to Replace"
2555 replace_to
: StringProperty(
2559 def execute(self
, context
):
2560 nodes
, links
= get_nodes_links(context
)
2561 for node
in [n
for n
in nodes
if n
.select
]:
2562 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
2566 def invoke(self
, context
, event
):
2570 return context
.window_manager
.invoke_props_dialog(self
)
2573 class NWAddTextureSetup(Operator
, NWBase
):
2574 bl_idname
= "node.nw_add_texture"
2575 bl_label
= "Texture Setup"
2576 bl_description
= "Add Texture Node Setup to Selected Shaders"
2577 bl_options
= {'REGISTER', 'UNDO'}
2579 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
2582 def poll(cls
, context
):
2584 if nw_check(context
):
2585 space
= context
.space_data
2586 if space
.tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
2590 def execute(self
, context
):
2591 nodes
, links
= get_nodes_links(context
)
2592 shader_types
= [x
[1] for x
in shaders_shader_nodes_props
if x
[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2593 texture_types
= [x
[1] for x
in shaders_texture_nodes_props
]
2594 selected_nodes
= [n
for n
in nodes
if n
.select
]
2595 for t_node
in selected_nodes
:
2599 for index
, i
in enumerate(t_node
.inputs
):
2605 locx
= t_node
.location
.x
2606 locy
= t_node
.location
.y
- t_node
.dimensions
.y
/2
2608 xoffset
= [500, 700]
2610 if t_node
.type in texture_types
+ ['MAPPING']:
2611 xoffset
= [290, 500]
2615 image_type
= 'ShaderNodeTexImage'
2617 if (t_node
.type in texture_types
and t_node
.type != 'TEX_IMAGE') or (t_node
.type == 'BACKGROUND'):
2618 coordout
= 0 # image texture uses UVs, procedural textures and Background shader use Generated
2619 if t_node
.type == 'BACKGROUND':
2620 image_type
= 'ShaderNodeTexEnvironment'
2623 tex
= nodes
.new(image_type
)
2624 tex
.location
= [locx
- 200, locy
+ 112]
2626 links
.new(tex
.outputs
[0], t_node
.inputs
[input_index
])
2628 t_node
.select
= False
2629 if self
.add_mapping
or is_texture
:
2630 if t_node
.type != 'MAPPING':
2631 m
= nodes
.new('ShaderNodeMapping')
2632 m
.location
= [locx
- xoffset
[0], locy
+ 141]
2636 coord
= nodes
.new('ShaderNodeTexCoord')
2637 coord
.location
= [locx
- (200 if t_node
.type == 'MAPPING' else xoffset
[1]), locy
+ 124]
2640 links
.new(m
.outputs
[0], tex
.inputs
[0])
2641 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
2644 links
.new(m
.outputs
[0], t_node
.inputs
[input_index
])
2645 links
.new(coord
.outputs
[coordout
], m
.inputs
[0])
2647 self
.report({'WARNING'}, "No free inputs for node: "+t_node
.name
)
2651 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
2652 bl_idname
= "node.nw_add_textures_for_principled"
2653 bl_label
= "Principled Texture Setup"
2654 bl_description
= "Add Texture Node Setup for Principled BSDF"
2655 bl_options
= {'REGISTER', 'UNDO'}
2657 directory
: StringProperty(
2661 description
='Folder to search in for image files'
2663 files
: CollectionProperty(
2664 type=bpy
.types
.OperatorFileListElement
,
2665 options
={'HIDDEN', 'SKIP_SAVE'}
2668 relative_path
: BoolProperty(
2669 name
='Relative Path',
2670 description
='Select the file relative to the blend file',
2679 def draw(self
, context
):
2680 layout
= self
.layout
2681 layout
.alignment
= 'LEFT'
2683 layout
.prop(self
, 'relative_path')
2686 def poll(cls
, context
):
2688 if nw_check(context
):
2689 space
= context
.space_data
2690 if space
.tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
2694 def execute(self
, context
):
2695 # Check if everything is ok
2696 if not self
.directory
:
2697 self
.report({'INFO'}, 'No Folder Selected')
2698 return {'CANCELLED'}
2699 if not self
.files
[:]:
2700 self
.report({'INFO'}, 'No Files Selected')
2701 return {'CANCELLED'}
2703 nodes
, links
= get_nodes_links(context
)
2704 active_node
= nodes
.active
2705 if not active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled':
2706 self
.report({'INFO'}, 'Select Principled BSDF')
2707 return {'CANCELLED'}
2710 def split_into__components(fname
):
2711 # Split filename into components
2712 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2714 fname
= path
.splitext(fname
)[0]
2716 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
2717 # Separate CamelCase by space
2718 fname
= re
.sub("([a-z])([A-Z])","\g<1> \g<2>",fname
)
2719 # Replace common separators with SPACE
2720 seperators
= ['_', '.', '-', '__', '--', '#']
2721 for sep
in seperators
:
2722 fname
= fname
.replace(sep
, ' ')
2724 components
= fname
.split(' ')
2725 components
= [c
.lower() for c
in components
]
2728 # Filter textures names for texturetypes in filenames
2729 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2730 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
2731 normal_abbr
= tags
.normal
.split(' ')
2732 bump_abbr
= tags
.bump
.split(' ')
2733 gloss_abbr
= tags
.gloss
.split(' ')
2734 rough_abbr
= tags
.rough
.split(' ')
2736 ['Displacement', tags
.displacement
.split(' '), None],
2737 ['Base Color', tags
.base_color
.split(' '), None],
2738 ['Subsurface Color', tags
.sss_color
.split(' '), None],
2739 ['Metallic', tags
.metallic
.split(' '), None],
2740 ['Specular', tags
.specular
.split(' '), None],
2741 ['Roughness', rough_abbr
+ gloss_abbr
, None],
2742 ['Normal', normal_abbr
+ bump_abbr
, None],
2745 # Look through texture_types and set value as filename of first matched file
2746 def match_files_to_socket_names():
2747 for sname
in socketnames
:
2748 for file in self
.files
:
2750 filenamecomponents
= split_into__components(fname
)
2751 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
2752 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2757 match_files_to_socket_names()
2758 # Remove socketnames without found files
2759 socketnames
= [s
for s
in socketnames
if s
[2]
2760 and path
.exists(self
.directory
+s
[2])]
2762 self
.report({'INFO'}, 'No matching images found')
2763 print('No matching images found')
2764 return {'CANCELLED'}
2766 # Don't override path earlier as os.path is used to check the absolute path
2767 import_path
= self
.directory
2768 if self
.relative_path
:
2769 if bpy
.data
.filepath
:
2770 import_path
= bpy
.path
.relpath(self
.directory
)
2772 self
.report({'WARNING'}, 'Relative paths cannot be used with unsaved scenes!')
2773 print('Relative paths cannot be used with unsaved scenes!')
2776 print('\nMatched Textures:')
2780 roughness_node
= None
2781 for i
, sname
in enumerate(socketnames
):
2782 print(i
, sname
[0], sname
[2])
2784 # DISPLACEMENT NODES
2785 if sname
[0] == 'Displacement':
2786 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
2787 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2788 disp_texture
.image
= img
2789 disp_texture
.label
= 'Displacement'
2790 if disp_texture
.image
:
2791 disp_texture
.image
.colorspace_settings
.is_data
= True
2793 # Add displacement offset nodes
2794 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
2795 disp_node
.location
= active_node
.location
+ Vector((0, -560))
2796 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
2798 # TODO Turn on true displacement in the material
2799 # Too complicated for now
2802 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
2804 if not output_node
[0].inputs
[2].is_linked
:
2805 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
2809 if not active_node
.inputs
[sname
[0]].is_linked
:
2810 # No texture node connected -> add texture node with new image
2811 texture_node
= nodes
.new(type='ShaderNodeTexImage')
2812 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2813 texture_node
.image
= img
2816 if sname
[0] == 'Normal':
2817 # Test if new texture node is normal or bump map
2818 fname_components
= split_into__components(sname
[2])
2819 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
2820 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
2822 # If Normal add normal node in between
2823 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
2824 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
2826 # If Bump add bump node in between
2827 normal_node
= nodes
.new(type='ShaderNodeBump')
2828 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
2830 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
2831 normal_node_texture
= texture_node
2833 elif sname
[0] == 'Roughness':
2834 # Test if glossy or roughness map
2835 fname_components
= split_into__components(sname
[2])
2836 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
2837 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
2840 # If Roughness nothing to to
2841 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2844 # If Gloss Map add invert node
2845 invert_node
= nodes
.new(type='ShaderNodeInvert')
2846 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
2848 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
2849 roughness_node
= texture_node
2852 # This is a simple connection Texture --> Input slot
2853 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2855 # Use non-color for all but 'Base Color' Textures
2856 if not sname
[0] in ['Base Color'] and texture_node
.image
:
2857 texture_node
.image
.colorspace_settings
.is_data
= True
2860 # If already texture connected. add to node list for alignment
2861 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
2863 # This are all connected texture nodes
2864 texture_nodes
.append(texture_node
)
2865 texture_node
.label
= sname
[0]
2868 texture_nodes
.append(disp_texture
)
2871 for i
, texture_node
in enumerate(texture_nodes
):
2872 offset
= Vector((-550, (i
* -280) + 200))
2873 texture_node
.location
= active_node
.location
+ offset
2876 # Extra alignment if normal node was added
2877 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
2880 # Alignment of invert node if glossy map
2881 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
2883 # Add texture input + mapping
2884 mapping
= nodes
.new(type='ShaderNodeMapping')
2885 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
2886 if len(texture_nodes
) > 1:
2887 # If more than one texture add reroute node in between
2888 reroute
= nodes
.new(type='NodeReroute')
2889 texture_nodes
.append(reroute
)
2890 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
2891 reroute
.location
= tex_coords
+ Vector((-50, -120))
2892 for texture_node
in texture_nodes
:
2893 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
2894 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
2896 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
2898 # Connect texture_coordiantes to mapping node
2899 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
2900 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
2901 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
2903 # Create frame around tex coords and mapping
2904 frame
= nodes
.new(type='NodeFrame')
2905 frame
.label
= 'Mapping'
2906 mapping
.parent
= frame
2907 texture_input
.parent
= frame
2910 # Create frame around texture nodes
2911 frame
= nodes
.new(type='NodeFrame')
2912 frame
.label
= 'Textures'
2913 for tnode
in texture_nodes
:
2914 tnode
.parent
= frame
2918 active_node
.select
= False
2921 force_update(context
)
2925 class NWAddReroutes(Operator
, NWBase
):
2926 """Add Reroute Nodes and link them to outputs of selected nodes"""
2927 bl_idname
= "node.nw_add_reroutes"
2928 bl_label
= "Add Reroutes"
2929 bl_description
= "Add Reroutes to Outputs"
2930 bl_options
= {'REGISTER', 'UNDO'}
2932 option
: EnumProperty(
2935 ('ALL', 'to all', 'Add to all outputs'),
2936 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2937 ('LINKED', 'to linked', 'Add only to linked outputs'),
2941 def execute(self
, context
):
2942 tree_type
= context
.space_data
.node_tree
.type
2943 option
= self
.option
2944 nodes
, links
= get_nodes_links(context
)
2945 # output valid when option is 'all' or when 'loose' output has no links
2947 post_select
= [] # nodes to be selected after execution
2948 # create reroutes and recreate links
2949 for node
in [n
for n
in nodes
if n
.select
]:
2954 # unhide 'REROUTE' nodes to avoid issues with location.y
2955 if node
.type == 'REROUTE':
2957 # When node is hidden - width_hidden not usable.
2958 # Hack needed to calculate real width
2960 bpy
.ops
.node
.select_all(action
='DESELECT')
2961 helper
= nodes
.new('NodeReroute')
2962 helper
.select
= True
2964 # resize node and helper to zero. Then check locations to calculate width
2965 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2966 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2967 # restore node location
2968 node
.location
= x
, y
2971 # only helper is selected now
2972 bpy
.ops
.node
.delete()
2973 x
= node
.location
.x
+ width
+ 20.0
2974 if node
.type != 'REROUTE':
2978 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
2979 for out_i
, output
in enumerate(node
.outputs
):
2980 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
2981 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
2982 if node
.type != 'R_LAYERS':
2984 else: # if 'R_LAYERS' check if output represent used render pass
2985 node_scene
= node
.scene
2986 node_layer
= node
.layer
2987 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2988 if output
.name
== 'Alpha':
2991 # check entries in global 'rl_outputs' variable
2992 for rlo
in rl_outputs
:
2993 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
2994 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
2997 valid
= ((option
== 'ALL') or
2998 (option
== 'LOOSE' and not output
.links
) or
2999 (option
== 'LINKED' and output
.links
))
3000 # Add reroutes only if valid, but offset location in all cases.
3002 n
= nodes
.new('NodeReroute')
3004 for link
in output
.links
:
3005 links
.new(n
.outputs
[0], link
.to_socket
)
3006 links
.new(output
, n
.inputs
[0])
3008 post_select
.append(n
)
3012 # disselect the node so that after execution of script only newly created nodes are selected
3014 # nicer reroutes distribution along y when node.hide
3016 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3017 for reroute
in [r
for r
in nodes
if r
.select
]:
3018 reroute
.location
.y
-= y_translate
3019 for node
in post_select
:
3025 class NWLinkActiveToSelected(Operator
, NWBase
):
3026 """Link active node to selected nodes basing on various criteria"""
3027 bl_idname
= "node.nw_link_active_to_selected"
3028 bl_label
= "Link Active Node to Selected"
3029 bl_options
= {'REGISTER', 'UNDO'}
3031 replace
: BoolProperty()
3032 use_node_name
: BoolProperty()
3033 use_outputs_names
: BoolProperty()
3036 def poll(cls
, context
):
3038 if nw_check(context
):
3039 if context
.active_node
is not None:
3040 if context
.active_node
.select
:
3044 def execute(self
, context
):
3045 nodes
, links
= get_nodes_links(context
)
3046 replace
= self
.replace
3047 use_node_name
= self
.use_node_name
3048 use_outputs_names
= self
.use_outputs_names
3049 active
= nodes
.active
3050 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3051 outputs
= [] # Only usable outputs of active nodes will be stored here.
3052 for out
in active
.outputs
:
3053 if active
.type != 'R_LAYERS':
3056 # 'R_LAYERS' node type needs special handling.
3057 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3058 # Only outputs that represent used passes should be taken into account
3059 # Check if pass represented by output is used.
3060 # global 'rl_outputs' list will be used for that
3061 for rlo
in rl_outputs
:
3062 pass_used
= False # initial value. Will be set to True if pass is used
3063 if out
.name
== 'Alpha':
3064 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3066 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3067 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3068 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3072 doit
= True # Will be changed to False when links successfully added to previous output.
3075 for node
in selected
:
3076 dst_name
= node
.name
# Will be compared with src_name if needed.
3077 # When node has label - use it as dst_name
3079 dst_name
= node
.label
3080 valid
= True # Initial value. Will be changed to False if names don't match.
3081 src_name
= dst_name
# If names not used - this asignment will keep valid = True.
3083 # Set src_name to source node name or label
3084 src_name
= active
.name
3086 src_name
= active
.label
3087 elif use_outputs_names
:
3088 src_name
= (out
.name
, )
3089 for rlo
in rl_outputs
:
3090 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3091 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3092 if dst_name
not in src_name
:
3095 for input in node
.inputs
:
3096 if input.type == out
.type or node
.type == 'REROUTE':
3097 if replace
or not input.is_linked
:
3098 links
.new(out
, input)
3099 if not use_node_name
and not use_outputs_names
:
3106 class NWAlignNodes(Operator
, NWBase
):
3107 '''Align the selected nodes neatly in a row/column'''
3108 bl_idname
= "node.nw_align_nodes"
3109 bl_label
= "Align Nodes"
3110 bl_options
= {'REGISTER', 'UNDO'}
3111 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3113 def execute(self
, context
):
3114 nodes
, links
= get_nodes_links(context
)
3115 margin
= self
.margin
3119 if node
.select
and node
.type != 'FRAME':
3120 selection
.append(node
)
3122 # If no nodes are selected, align all nodes
3126 elif nodes
.active
in selection
:
3127 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3129 # Check if nodes should be laid out horizontally or vertically
3130 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3131 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3132 x_range
= max(x_locs
) - min(x_locs
)
3133 y_range
= max(y_locs
) - min(y_locs
)
3134 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3135 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3136 horizontal
= x_range
> y_range
3138 # Sort selection by location of node mid-point
3140 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3142 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3146 for node
in selection
:
3147 current_margin
= margin
3148 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3151 node
.location
.x
= current_pos
3152 current_pos
+= current_margin
+ node
.dimensions
.x
3153 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3155 node
.location
.y
= current_pos
3156 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3157 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3159 # If active node is selected, center nodes around it
3160 if active_loc
is not None:
3161 active_loc_diff
= active_loc
- nodes
.active
.location
3162 for node
in selection
:
3163 node
.location
+= active_loc_diff
3164 else: # Position nodes centered around where they used to be
3165 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
])
3166 new_mid
= (max(locs
) + min(locs
)) / 2
3167 for node
in selection
:
3169 node
.location
.x
+= (mid_x
- new_mid
)
3171 node
.location
.y
+= (mid_y
- new_mid
)
3176 class NWSelectParentChildren(Operator
, NWBase
):
3177 bl_idname
= "node.nw_select_parent_child"
3178 bl_label
= "Select Parent or Children"
3179 bl_options
= {'REGISTER', 'UNDO'}
3181 option
: EnumProperty(
3184 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3185 ('CHILD', 'Select Children', 'Select members of selected frame'),
3189 def execute(self
, context
):
3190 nodes
, links
= get_nodes_links(context
)
3191 option
= self
.option
3192 selected
= [node
for node
in nodes
if node
.select
]
3193 if option
== 'PARENT':
3194 for sel
in selected
:
3197 parent
.select
= True
3198 else: # option == 'CHILD'
3199 for sel
in selected
:
3200 children
= [node
for node
in nodes
if node
.parent
== sel
]
3201 for kid
in children
:
3207 class NWDetachOutputs(Operator
, NWBase
):
3208 """Detach outputs of selected node leaving inputs linked"""
3209 bl_idname
= "node.nw_detach_outputs"
3210 bl_label
= "Detach Outputs"
3211 bl_options
= {'REGISTER', 'UNDO'}
3213 def execute(self
, context
):
3214 nodes
, links
= get_nodes_links(context
)
3215 selected
= context
.selected_nodes
3216 bpy
.ops
.node
.duplicate_move_keep_inputs()
3217 new_nodes
= context
.selected_nodes
3218 bpy
.ops
.node
.select_all(action
="DESELECT")
3219 for node
in selected
:
3221 bpy
.ops
.node
.delete_reconnect()
3222 for new_node
in new_nodes
:
3223 new_node
.select
= True
3224 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3229 class NWLinkToOutputNode(Operator
, NWBase
):
3230 """Link to Composite node or Material Output node"""
3231 bl_idname
= "node.nw_link_out"
3232 bl_label
= "Connect to Output"
3233 bl_options
= {'REGISTER', 'UNDO'}
3236 def poll(cls
, context
):
3238 if nw_check(context
):
3239 if context
.active_node
is not None:
3240 for out
in context
.active_node
.outputs
:
3241 if is_visible_socket(out
):
3246 def execute(self
, context
):
3247 nodes
, links
= get_nodes_links(context
)
3248 active
= nodes
.active
3251 tree_type
= context
.space_data
.tree_type
3252 output_types_shaders
= [x
[1] for x
in shaders_output_nodes_props
]
3253 output_types_compo
= ['COMPOSITE']
3254 output_types_blender_mat
= ['OUTPUT']
3255 output_types_textures
= ['OUTPUT']
3256 output_types
= output_types_shaders
+ output_types_compo
+ output_types_blender_mat
3258 if node
.type in output_types
:
3262 bpy
.ops
.node
.select_all(action
="DESELECT")
3263 if tree_type
== 'ShaderNodeTree':
3264 if is_cycles_or_eevee(context
):
3265 output_node
= nodes
.new('ShaderNodeOutputMaterial')
3267 output_node
= nodes
.new('ShaderNodeOutput')
3268 elif tree_type
== 'CompositorNodeTree':
3269 output_node
= nodes
.new('CompositorNodeComposite')
3270 elif tree_type
== 'TextureNodeTree':
3271 output_node
= nodes
.new('TextureNodeOutput')
3272 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3273 output_node
.location
.y
= active
.location
.y
3274 if (output_node
and active
.outputs
):
3275 for i
, output
in enumerate(active
.outputs
):
3276 if is_visible_socket(output
):
3279 for i
, output
in enumerate(active
.outputs
):
3280 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3285 if tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
3286 if active
.outputs
[output_index
].name
== 'Volume':
3288 elif active
.outputs
[output_index
].type != 'SHADER': # connect to displacement if not a shader
3290 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3292 force_update(context
) # viewport render does not update
3297 class NWMakeLink(Operator
, NWBase
):
3298 """Make a link from one socket to another"""
3299 bl_idname
= 'node.nw_make_link'
3300 bl_label
= 'Make Link'
3301 bl_options
= {'REGISTER', 'UNDO'}
3302 from_socket
: IntProperty()
3303 to_socket
: IntProperty()
3305 def execute(self
, context
):
3306 nodes
, links
= get_nodes_links(context
)
3308 n1
= nodes
[context
.scene
.NWLazySource
]
3309 n2
= nodes
[context
.scene
.NWLazyTarget
]
3311 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3313 force_update(context
)
3318 class NWCallInputsMenu(Operator
, NWBase
):
3319 """Link from this output"""
3320 bl_idname
= 'node.nw_call_inputs_menu'
3321 bl_label
= 'Make Link'
3322 bl_options
= {'REGISTER', 'UNDO'}
3323 from_socket
: IntProperty()
3325 def execute(self
, context
):
3326 nodes
, links
= get_nodes_links(context
)
3328 context
.scene
.NWSourceSocket
= self
.from_socket
3330 n1
= nodes
[context
.scene
.NWLazySource
]
3331 n2
= nodes
[context
.scene
.NWLazyTarget
]
3332 if len(n2
.inputs
) > 1:
3333 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3334 elif len(n2
.inputs
) == 1:
3335 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3339 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3340 """Add an Image Sequence"""
3341 bl_idname
= 'node.nw_add_sequence'
3342 bl_label
= 'Import Image Sequence'
3343 bl_options
= {'REGISTER', 'UNDO'}
3345 directory
: StringProperty(
3348 filename
: StringProperty(
3351 files
: CollectionProperty(
3352 type=bpy
.types
.OperatorFileListElement
,
3353 options
={'HIDDEN', 'SKIP_SAVE'}
3356 def execute(self
, context
):
3357 nodes
, links
= get_nodes_links(context
)
3358 directory
= self
.directory
3359 filename
= self
.filename
3361 tree
= context
.space_data
.node_tree
3364 # print ("\nDIR:", directory)
3365 # print ("FN:", filename)
3366 # print ("Fs:", list(f.name for f in files), '\n')
3368 if tree
.type == 'SHADER':
3369 node_type
= "ShaderNodeTexImage"
3370 elif tree
.type == 'COMPOSITING':
3371 node_type
= "CompositorNodeImage"
3373 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3374 return {'CANCELLED'}
3376 if not files
[0].name
and not filename
:
3377 self
.report({'ERROR'}, "No file chosen")
3378 return {'CANCELLED'}
3379 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3380 # User has selected multiple files without an active one, or the active one is non-existant
3381 filename
= files
[0].name
3383 if not path
.exists(directory
+filename
):
3384 self
.report({'ERROR'}, filename
+" does not exist!")
3385 return {'CANCELLED'}
3387 without_ext
= '.'.join(filename
.split('.')[:-1])
3389 # if last digit isn't a number, it's not a sequence
3390 if not without_ext
[-1].isdigit():
3391 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3392 return {'CANCELLED'}
3395 extension
= filename
.split('.')[-1]
3396 reverse
= without_ext
[::-1] # reverse string
3399 for char
in reverse
:
3405 without_num
= without_ext
[:count_numbers
*-1]
3407 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3409 num_frames
= len(files
)
3411 nodes_list
= [node
for node
in nodes
]
3413 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3414 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3418 yloc
+= node_mid_pt(node
, 'y')
3419 yloc
= yloc
/len(nodes
)
3424 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3426 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3428 node
.label
= name_with_hashes
3430 img
= bpy
.data
.images
.load(directory
+(without_ext
+'.'+extension
))
3431 img
.source
= 'SEQUENCE'
3432 img
.name
= name_with_hashes
3434 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3435 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
3436 image_user
.frame_duration
= num_frames
3441 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3442 """Add multiple images at once"""
3443 bl_idname
= 'node.nw_add_multiple_images'
3444 bl_label
= 'Open Selected Images'
3445 bl_options
= {'REGISTER', 'UNDO'}
3446 directory
: StringProperty(
3449 files
: CollectionProperty(
3450 type=bpy
.types
.OperatorFileListElement
,
3451 options
={'HIDDEN', 'SKIP_SAVE'}
3454 def execute(self
, context
):
3455 nodes
, links
= get_nodes_links(context
)
3457 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3459 if context
.space_data
.node_tree
.type == 'SHADER':
3460 node_type
= "ShaderNodeTexImage"
3461 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3462 node_type
= "CompositorNodeImage"
3464 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3465 return {'CANCELLED'}
3468 for f
in self
.files
:
3471 node
= nodes
.new(node_type
)
3472 new_nodes
.append(node
)
3475 node
.width_hidden
= 100
3476 node
.location
.x
= xloc
3477 node
.location
.y
= yloc
3480 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3483 # shift new nodes up to center of tree
3484 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3486 if node
in new_nodes
:
3488 node
.location
.y
+= (list_size
/2)
3494 class NWViewerFocus(bpy
.types
.Operator
):
3495 """Set the viewer tile center to the mouse position"""
3496 bl_idname
= "node.nw_viewer_focus"
3497 bl_label
= "Viewer Focus"
3499 x
: bpy
.props
.IntProperty()
3500 y
: bpy
.props
.IntProperty()
3503 def poll(cls
, context
):
3504 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
3506 def execute(self
, context
):
3509 def invoke(self
, context
, event
):
3510 render
= context
.scene
.render
3511 space
= context
.space_data
3512 percent
= render
.resolution_percentage
*0.01
3514 nodes
, links
= get_nodes_links(context
)
3515 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
3518 mlocx
= event
.mouse_region_x
3519 mlocy
= event
.mouse_region_y
3520 select_node
= bpy
.ops
.node
.select(mouse_x
=mlocx
, mouse_y
=mlocy
, extend
=False)
3522 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
3523 region_x
= context
.region
.width
3524 region_y
= context
.region
.height
3526 region_center_x
= context
.region
.width
/ 2
3527 region_center_y
= context
.region
.height
/ 2
3529 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
3530 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
3532 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
3533 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
3535 margin_x
= region_center_x
- backdrop_center_x
3536 margin_y
= region_center_y
- backdrop_center_y
3538 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
3539 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
3541 for node
in viewers
:
3542 node
.center_x
= abs_mouse_x
3543 node
.center_y
= abs_mouse_y
3545 return {'PASS_THROUGH'}
3547 return self
.execute(context
)
3550 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
3551 """Save the current viewer node to an image file"""
3552 bl_idname
= "node.nw_save_viewer"
3553 bl_label
= "Save This Image"
3554 filepath
: StringProperty(subtype
="FILE_PATH")
3555 filename_ext
: EnumProperty(
3557 description
="Choose the file format to save to",
3558 items
=(('.bmp', "PNG", ""),
3559 ('.rgb', 'IRIS', ""),
3560 ('.png', 'PNG', ""),
3561 ('.jpg', 'JPEG', ""),
3562 ('.jp2', 'JPEG2000', ""),
3563 ('.tga', 'TARGA', ""),
3564 ('.cin', 'CINEON', ""),
3565 ('.dpx', 'DPX', ""),
3566 ('.exr', 'OPEN_EXR', ""),
3567 ('.hdr', 'HDR', ""),
3568 ('.tif', 'TIFF', "")),
3573 def poll(cls
, context
):
3575 if nw_check(context
):
3576 if context
.space_data
.tree_type
== 'CompositorNodeTree':
3577 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
3578 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
3582 def execute(self
, context
):
3599 basename
, ext
= path
.splitext(fp
)
3600 old_render_format
= context
.scene
.render
.image_settings
.file_format
3601 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
3602 context
.area
.type = "IMAGE_EDITOR"
3603 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
3604 context
.area
.spaces
[0].image
.save_render(fp
)
3605 context
.area
.type = "NODE_EDITOR"
3606 context
.scene
.render
.image_settings
.file_format
= old_render_format
3610 class NWResetNodes(bpy
.types
.Operator
):
3611 """Reset Nodes in Selection"""
3612 bl_idname
= "node.nw_reset_nodes"
3613 bl_label
= "Reset Nodes"
3614 bl_options
= {'REGISTER', 'UNDO'}
3617 def poll(cls
, context
):
3618 space
= context
.space_data
3619 return space
.type == 'NODE_EDITOR'
3621 def execute(self
, context
):
3622 node_active
= context
.active_node
3623 node_selected
= context
.selected_nodes
3624 node_ignore
= ["FRAME","REROUTE", "GROUP"]
3626 # Check if one node is selected at least
3627 if not (len(node_selected
) > 0):
3628 self
.report({'ERROR'}, "1 node must be selected at least")
3629 return {'CANCELLED'}
3631 active_node_name
= node_active
.name
if node_active
.select
else None
3632 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
3634 # Create output lists
3635 selected_node_names
= [n
.name
for n
in node_selected
]
3638 # Reset all valid children in a frame
3639 node_active_is_frame
= False
3640 if len(node_selected
) == 1 and node_active
.type == "FRAME":
3641 node_tree
= node_active
.id_data
3642 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
3644 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
3645 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
3646 node_active_is_frame
= True
3648 # Check if valid nodes in selection
3649 if not (len(valid_nodes
) > 0):
3650 # Check for frames only
3651 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
3652 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
3653 self
.report({'ERROR'}, "Please select only 1 frame to reset")
3655 self
.report({'ERROR'}, "No valid node(s) in selection")
3656 return {'CANCELLED'}
3658 # Report nodes that are not valid
3659 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
3660 valid_node_names
= [n
.name
for n
in valid_nodes
]
3661 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
3662 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
3664 # Deselect all nodes
3665 for i
in node_selected
:
3668 # Run through all valid nodes
3669 for node
in valid_nodes
:
3671 parent
= node
.parent
if node
.parent
else None
3672 node_loc
= [node
.location
.x
, node
.location
.y
]
3674 node_tree
= node
.id_data
3675 props_to_copy
= 'bl_idname name location height width'.split(' ')
3678 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
3679 for i
in (i
for i
in mappings
if i
.is_linked
):
3681 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
3683 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
3685 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
3686 props_to_copy
.pop(0)
3688 for prop
in props_to_copy
:
3689 setattr(new_node
, prop
, props
[prop
])
3691 nodes
= node_tree
.nodes
3693 new_node
.name
= props
['name']
3696 new_node
.parent
= parent
3697 new_node
.location
= node_loc
3699 for str_from
, str_to
in reconnections
:
3700 node_tree
.links
.new(eval(str_from
), eval(str_to
))
3702 new_node
.select
= False
3703 success_names
.append(new_node
.name
)
3705 # Reselect all nodes
3706 if selected_node_names
and node_active_is_frame
is False:
3707 for i
in selected_node_names
:
3708 node_tree
.nodes
[i
].select
= True
3710 if active_node_name
is not None:
3711 node_tree
.nodes
[active_node_name
].select
= True
3712 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
3714 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
3722 def drawlayout(context
, layout
, mode
='non-panel'):
3723 tree_type
= context
.space_data
.tree_type
3725 col
= layout
.column(align
=True)
3726 col
.menu(NWMergeNodesMenu
.bl_idname
)
3729 col
= layout
.column(align
=True)
3730 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
3733 if tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
):
3734 col
= layout
.column(align
=True)
3735 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
3736 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
3739 col
= layout
.column(align
=True)
3740 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
3741 col
.operator(NWSwapLinks
.bl_idname
)
3742 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
3745 col
= layout
.column(align
=True)
3746 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
3747 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
3750 col
= layout
.column(align
=True)
3752 row
= col
.row(align
=True)
3753 row
.operator(NWClearLabel
.bl_idname
).option
= True
3754 row
.operator(NWModifyLabels
.bl_idname
)
3756 col
.operator(NWClearLabel
.bl_idname
).option
= True
3757 col
.operator(NWModifyLabels
.bl_idname
)
3758 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
3760 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
3763 col
= layout
.column(align
=True)
3764 if tree_type
== 'CompositorNodeTree':
3765 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
3766 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
3769 col
= layout
.column(align
=True)
3770 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
3773 col
= layout
.column(align
=True)
3774 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
3777 col
= layout
.column(align
=True)
3778 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
3782 class NodeWranglerPanel(Panel
, NWBase
):
3783 bl_idname
= "NODE_PT_nw_node_wrangler"
3784 bl_space_type
= 'NODE_EDITOR'
3785 bl_label
= "Node Wrangler"
3786 bl_region_type
= "UI"
3787 bl_category
= "Node Wrangler"
3789 prepend
: StringProperty(
3792 append
: StringProperty()
3793 remove
: StringProperty()
3795 def draw(self
, context
):
3796 self
.layout
.label(text
="(Quick access: Shift+W)")
3797 drawlayout(context
, self
.layout
, mode
='panel')
3803 class NodeWranglerMenu(Menu
, NWBase
):
3804 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
3805 bl_label
= "Node Wrangler"
3807 def draw(self
, context
):
3808 drawlayout(context
, self
.layout
)
3811 class NWMergeNodesMenu(Menu
, NWBase
):
3812 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
3813 bl_label
= "Merge Selected Nodes"
3815 def draw(self
, context
):
3816 type = context
.space_data
.tree_type
3817 layout
= self
.layout
3818 if type == 'ShaderNodeTree' and is_cycles_or_eevee(context
):
3819 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
3820 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
3821 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
3822 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
3824 props
.merge_type
= 'ZCOMBINE'
3825 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
3827 props
.merge_type
= 'ALPHAOVER'
3830 class NWMergeShadersMenu(Menu
, NWBase
):
3831 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
3832 bl_label
= "Merge Selected Nodes using Shaders"
3834 def draw(self
, context
):
3835 layout
= self
.layout
3836 for type in ('MIX', 'ADD'):
3837 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
3839 props
.merge_type
= 'SHADER'
3842 class NWMergeMixMenu(Menu
, NWBase
):
3843 bl_idname
= "NODE_MT_nw_merge_mix_menu"
3844 bl_label
= "Merge Selected Nodes using Mix"
3846 def draw(self
, context
):
3847 layout
= self
.layout
3848 for type, name
, description
in blend_types
:
3849 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3851 props
.merge_type
= 'MIX'
3854 class NWConnectionListOutputs(Menu
, NWBase
):
3855 bl_idname
= "NODE_MT_nw_connection_list_out"
3858 def draw(self
, context
):
3859 layout
= self
.layout
3860 nodes
, links
= get_nodes_links(context
)
3862 n1
= nodes
[context
.scene
.NWLazySource
]
3864 if n1
.type == "R_LAYERS":
3866 for o
in n1
.outputs
:
3867 if o
.enabled
: # Check which passes the render layer has enabled
3868 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
3872 for o
in n1
.outputs
:
3873 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=o
.name
, icon
="RADIOBUT_OFF").from_socket
=index
3877 class NWConnectionListInputs(Menu
, NWBase
):
3878 bl_idname
= "NODE_MT_nw_connection_list_in"
3881 def draw(self
, context
):
3882 layout
= self
.layout
3883 nodes
, links
= get_nodes_links(context
)
3885 n2
= nodes
[context
.scene
.NWLazyTarget
]
3889 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=i
.name
, icon
="FORWARD")
3890 op
.from_socket
= context
.scene
.NWSourceSocket
3891 op
.to_socket
= index
3895 class NWMergeMathMenu(Menu
, NWBase
):
3896 bl_idname
= "NODE_MT_nw_merge_math_menu"
3897 bl_label
= "Merge Selected Nodes using Math"
3899 def draw(self
, context
):
3900 layout
= self
.layout
3901 for type, name
, description
in operations
:
3902 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3904 props
.merge_type
= 'MATH'
3907 class NWBatchChangeNodesMenu(Menu
, NWBase
):
3908 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
3909 bl_label
= "Batch Change Selected Nodes"
3911 def draw(self
, context
):
3912 layout
= self
.layout
3913 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
3914 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
3917 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
3918 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
3919 bl_label
= "Batch Change Blend Type"
3921 def draw(self
, context
):
3922 layout
= self
.layout
3923 for type, name
, description
in blend_types
:
3924 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3925 props
.blend_type
= type
3926 props
.operation
= 'CURRENT'
3929 class NWBatchChangeOperationMenu(Menu
, NWBase
):
3930 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
3931 bl_label
= "Batch Change Math Operation"
3933 def draw(self
, context
):
3934 layout
= self
.layout
3935 for type, name
, description
in operations
:
3936 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3937 props
.blend_type
= 'CURRENT'
3938 props
.operation
= type
3941 class NWCopyToSelectedMenu(Menu
, NWBase
):
3942 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
3943 bl_label
= "Copy to Selected"
3945 def draw(self
, context
):
3946 layout
= self
.layout
3947 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
3948 layout
.menu(NWCopyLabelMenu
.bl_idname
)
3951 class NWCopyLabelMenu(Menu
, NWBase
):
3952 bl_idname
= "NODE_MT_nw_copy_label_menu"
3953 bl_label
= "Copy Label"
3955 def draw(self
, context
):
3956 layout
= self
.layout
3957 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
3958 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
3959 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
3962 class NWAddReroutesMenu(Menu
, NWBase
):
3963 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
3964 bl_label
= "Add Reroutes"
3965 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
3967 def draw(self
, context
):
3968 layout
= self
.layout
3969 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
3970 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
3971 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
3974 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
3975 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
3976 bl_label
= "Link Active to Selected"
3978 def draw(self
, context
):
3979 layout
= self
.layout
3980 layout
.menu(NWLinkStandardMenu
.bl_idname
)
3981 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
3982 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
3985 class NWLinkStandardMenu(Menu
, NWBase
):
3986 bl_idname
= "NODE_MT_nw_link_standard_menu"
3987 bl_label
= "To All Selected"
3989 def draw(self
, context
):
3990 layout
= self
.layout
3991 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
3992 props
.replace
= False
3993 props
.use_node_name
= False
3994 props
.use_outputs_names
= False
3995 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
3996 props
.replace
= True
3997 props
.use_node_name
= False
3998 props
.use_outputs_names
= False
4001 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4002 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4003 bl_label
= "Use Node Name/Label"
4005 def draw(self
, context
):
4006 layout
= self
.layout
4007 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4008 props
.replace
= False
4009 props
.use_node_name
= True
4010 props
.use_outputs_names
= False
4011 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4012 props
.replace
= True
4013 props
.use_node_name
= True
4014 props
.use_outputs_names
= False
4017 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4018 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4019 bl_label
= "Use Outputs Names"
4021 def draw(self
, context
):
4022 layout
= self
.layout
4023 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4024 props
.replace
= False
4025 props
.use_node_name
= False
4026 props
.use_outputs_names
= True
4027 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4028 props
.replace
= True
4029 props
.use_node_name
= False
4030 props
.use_outputs_names
= True
4033 class NWVertColMenu(bpy
.types
.Menu
):
4034 bl_idname
= "NODE_MT_nw_node_vertex_color_menu"
4035 bl_label
= "Vertex Colors"
4038 def poll(cls
, context
):
4040 if nw_check(context
):
4041 snode
= context
.space_data
4042 valid
= snode
.tree_type
== 'ShaderNodeTree' and is_cycles_or_eevee(context
)
4045 def draw(self
, context
):
4047 nodes
, links
= get_nodes_links(context
)
4048 mat
= context
.object.active_material
4051 for obj
in bpy
.data
.objects
:
4052 for slot
in obj
.material_slots
:
4053 if slot
.material
== mat
:
4057 if obj
.data
.vertex_colors
:
4058 for vcol
in obj
.data
.vertex_colors
:
4059 vcols
.append(vcol
.name
)
4060 vcols
= list(set(vcols
)) # get a unique list
4064 l
.operator(NWAddAttrNode
.bl_idname
, text
=vcol
).attr_name
= vcol
4066 l
.label(text
="No Vertex Color layers on objects with this material")
4069 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4070 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4071 bl_label
= "Switch Type to..."
4073 def draw(self
, context
):
4074 layout
= self
.layout
4075 tree
= context
.space_data
.node_tree
4076 if tree
.type == 'SHADER':
4077 if is_cycles_or_eevee(context
):
4078 layout
.menu(NWSwitchShadersInputSubmenu
.bl_idname
)
4079 layout
.menu(NWSwitchShadersOutputSubmenu
.bl_idname
)
4080 layout
.menu(NWSwitchShadersShaderSubmenu
.bl_idname
)
4081 layout
.menu(NWSwitchShadersTextureSubmenu
.bl_idname
)
4082 layout
.menu(NWSwitchShadersColorSubmenu
.bl_idname
)
4083 layout
.menu(NWSwitchShadersVectorSubmenu
.bl_idname
)
4084 layout
.menu(NWSwitchShadersConverterSubmenu
.bl_idname
)
4085 layout
.menu(NWSwitchShadersLayoutSubmenu
.bl_idname
)
4087 layout
.menu(NWSwitchMatInputSubmenu
.bl_idname
)
4088 layout
.menu(NWSwitchMatOutputSubmenu
.bl_idname
)
4089 layout
.menu(NWSwitchMatColorSubmenu
.bl_idname
)
4090 layout
.menu(NWSwitchMatVectorSubmenu
.bl_idname
)
4091 layout
.menu(NWSwitchMatConverterSubmenu
.bl_idname
)
4092 layout
.menu(NWSwitchMatLayoutSubmenu
.bl_idname
)
4093 if tree
.type == 'COMPOSITING':
4094 layout
.menu(NWSwitchCompoInputSubmenu
.bl_idname
)
4095 layout
.menu(NWSwitchCompoOutputSubmenu
.bl_idname
)
4096 layout
.menu(NWSwitchCompoColorSubmenu
.bl_idname
)
4097 layout
.menu(NWSwitchCompoConverterSubmenu
.bl_idname
)
4098 layout
.menu(NWSwitchCompoFilterSubmenu
.bl_idname
)
4099 layout
.menu(NWSwitchCompoVectorSubmenu
.bl_idname
)
4100 layout
.menu(NWSwitchCompoMatteSubmenu
.bl_idname
)
4101 layout
.menu(NWSwitchCompoDistortSubmenu
.bl_idname
)
4102 layout
.menu(NWSwitchCompoLayoutSubmenu
.bl_idname
)
4103 if tree
.type == 'TEXTURE':
4104 layout
.menu(NWSwitchTexInputSubmenu
.bl_idname
)
4105 layout
.menu(NWSwitchTexOutputSubmenu
.bl_idname
)
4106 layout
.menu(NWSwitchTexColorSubmenu
.bl_idname
)
4107 layout
.menu(NWSwitchTexPatternSubmenu
.bl_idname
)
4108 layout
.menu(NWSwitchTexTexturesSubmenu
.bl_idname
)
4109 layout
.menu(NWSwitchTexConverterSubmenu
.bl_idname
)
4110 layout
.menu(NWSwitchTexDistortSubmenu
.bl_idname
)
4111 layout
.menu(NWSwitchTexLayoutSubmenu
.bl_idname
)
4114 class NWSwitchShadersInputSubmenu(Menu
, NWBase
):
4115 bl_idname
= "NODE_MT_nw_switch_shaders_input_submenu"
4118 def draw(self
, context
):
4119 layout
= self
.layout
4120 for ident
, node_type
, rna_name
in sorted(shaders_input_nodes_props
, key
=lambda k
: k
[2]):
4121 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4122 props
.to_type
= ident
4125 class NWSwitchShadersOutputSubmenu(Menu
, NWBase
):
4126 bl_idname
= "NODE_MT_nw_switch_shaders_output_submenu"
4129 def draw(self
, context
):
4130 layout
= self
.layout
4131 for ident
, node_type
, rna_name
in sorted(shaders_output_nodes_props
, key
=lambda k
: k
[2]):
4132 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4133 props
.to_type
= ident
4136 class NWSwitchShadersShaderSubmenu(Menu
, NWBase
):
4137 bl_idname
= "NODE_MT_nw_switch_shaders_shader_submenu"
4140 def draw(self
, context
):
4141 layout
= self
.layout
4142 for ident
, node_type
, rna_name
in sorted(shaders_shader_nodes_props
, key
=lambda k
: k
[2]):
4143 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4144 props
.to_type
= ident
4147 class NWSwitchShadersTextureSubmenu(Menu
, NWBase
):
4148 bl_idname
= "NODE_MT_nw_switch_shaders_texture_submenu"
4149 bl_label
= "Texture"
4151 def draw(self
, context
):
4152 layout
= self
.layout
4153 for ident
, node_type
, rna_name
in sorted(shaders_texture_nodes_props
, key
=lambda k
: k
[2]):
4154 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4155 props
.to_type
= ident
4158 class NWSwitchShadersColorSubmenu(Menu
, NWBase
):
4159 bl_idname
= "NODE_MT_nw_switch_shaders_color_submenu"
4162 def draw(self
, context
):
4163 layout
= self
.layout
4164 for ident
, node_type
, rna_name
in sorted(shaders_color_nodes_props
, key
=lambda k
: k
[2]):
4165 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4166 props
.to_type
= ident
4169 class NWSwitchShadersVectorSubmenu(Menu
, NWBase
):
4170 bl_idname
= "NODE_MT_nw_switch_shaders_vector_submenu"
4173 def draw(self
, context
):
4174 layout
= self
.layout
4175 for ident
, node_type
, rna_name
in sorted(shaders_vector_nodes_props
, key
=lambda k
: k
[2]):
4176 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4177 props
.to_type
= ident
4180 class NWSwitchShadersConverterSubmenu(Menu
, NWBase
):
4181 bl_idname
= "NODE_MT_nw_switch_shaders_converter_submenu"
4182 bl_label
= "Converter"
4184 def draw(self
, context
):
4185 layout
= self
.layout
4186 for ident
, node_type
, rna_name
in sorted(shaders_converter_nodes_props
, key
=lambda k
: k
[2]):
4187 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4188 props
.to_type
= ident
4191 class NWSwitchShadersLayoutSubmenu(Menu
, NWBase
):
4192 bl_idname
= "NODE_MT_nw_switch_shaders_layout_submenu"
4195 def draw(self
, context
):
4196 layout
= self
.layout
4197 for ident
, node_type
, rna_name
in sorted(shaders_layout_nodes_props
, key
=lambda k
: k
[2]):
4198 if node_type
!= 'FRAME':
4199 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4200 props
.to_type
= ident
4203 class NWSwitchCompoInputSubmenu(Menu
, NWBase
):
4204 bl_idname
= "NODE_MT_nw_switch_compo_input_submenu"
4207 def draw(self
, context
):
4208 layout
= self
.layout
4209 for ident
, node_type
, rna_name
in sorted(compo_input_nodes_props
, key
=lambda k
: k
[2]):
4210 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4211 props
.to_type
= ident
4214 class NWSwitchCompoOutputSubmenu(Menu
, NWBase
):
4215 bl_idname
= "NODE_MT_nw_switch_compo_output_submenu"
4218 def draw(self
, context
):
4219 layout
= self
.layout
4220 for ident
, node_type
, rna_name
in sorted(compo_output_nodes_props
, key
=lambda k
: k
[2]):
4221 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4222 props
.to_type
= ident
4225 class NWSwitchCompoColorSubmenu(Menu
, NWBase
):
4226 bl_idname
= "NODE_MT_nw_switch_compo_color_submenu"
4229 def draw(self
, context
):
4230 layout
= self
.layout
4231 for ident
, node_type
, rna_name
in sorted(compo_color_nodes_props
, key
=lambda k
: k
[2]):
4232 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4233 props
.to_type
= ident
4236 class NWSwitchCompoConverterSubmenu(Menu
, NWBase
):
4237 bl_idname
= "NODE_MT_nw_switch_compo_converter_submenu"
4238 bl_label
= "Converter"
4240 def draw(self
, context
):
4241 layout
= self
.layout
4242 for ident
, node_type
, rna_name
in sorted(compo_converter_nodes_props
, key
=lambda k
: k
[2]):
4243 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4244 props
.to_type
= ident
4247 class NWSwitchCompoFilterSubmenu(Menu
, NWBase
):
4248 bl_idname
= "NODE_MT_nw_switch_compo_filter_submenu"
4251 def draw(self
, context
):
4252 layout
= self
.layout
4253 for ident
, node_type
, rna_name
in sorted(compo_filter_nodes_props
, key
=lambda k
: k
[2]):
4254 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4255 props
.to_type
= ident
4258 class NWSwitchCompoVectorSubmenu(Menu
, NWBase
):
4259 bl_idname
= "NODE_MT_nw_switch_compo_vector_submenu"
4262 def draw(self
, context
):
4263 layout
= self
.layout
4264 for ident
, node_type
, rna_name
in sorted(compo_vector_nodes_props
, key
=lambda k
: k
[2]):
4265 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4266 props
.to_type
= ident
4269 class NWSwitchCompoMatteSubmenu(Menu
, NWBase
):
4270 bl_idname
= "NODE_MT_nw_switch_compo_matte_submenu"
4273 def draw(self
, context
):
4274 layout
= self
.layout
4275 for ident
, node_type
, rna_name
in sorted(compo_matte_nodes_props
, key
=lambda k
: k
[2]):
4276 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4277 props
.to_type
= ident
4280 class NWSwitchCompoDistortSubmenu(Menu
, NWBase
):
4281 bl_idname
= "NODE_MT_nw_switch_compo_distort_submenu"
4282 bl_label
= "Distort"
4284 def draw(self
, context
):
4285 layout
= self
.layout
4286 for ident
, node_type
, rna_name
in sorted(compo_distort_nodes_props
, key
=lambda k
: k
[2]):
4287 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4288 props
.to_type
= ident
4291 class NWSwitchCompoLayoutSubmenu(Menu
, NWBase
):
4292 bl_idname
= "NODE_MT_nw_switch_compo_layout_submenu"
4295 def draw(self
, context
):
4296 layout
= self
.layout
4297 for ident
, node_type
, rna_name
in sorted(compo_layout_nodes_props
, key
=lambda k
: k
[2]):
4298 if node_type
!= 'FRAME':
4299 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4300 props
.to_type
= ident
4303 class NWSwitchMatInputSubmenu(Menu
, NWBase
):
4304 bl_idname
= "NODE_MT_nw_switch_mat_input_submenu"
4307 def draw(self
, context
):
4308 layout
= self
.layout
4309 for ident
, node_type
, rna_name
in sorted(blender_mat_input_nodes_props
, key
=lambda k
: k
[2]):
4310 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4311 props
.to_type
= ident
4314 class NWSwitchMatOutputSubmenu(Menu
, NWBase
):
4315 bl_idname
= "NODE_MT_nw_switch_mat_output_submenu"
4318 def draw(self
, context
):
4319 layout
= self
.layout
4320 for ident
, node_type
, rna_name
in sorted(blender_mat_output_nodes_props
, key
=lambda k
: k
[2]):
4321 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4322 props
.to_type
= ident
4325 class NWSwitchMatColorSubmenu(Menu
, NWBase
):
4326 bl_idname
= "NODE_MT_nw_switch_mat_color_submenu"
4329 def draw(self
, context
):
4330 layout
= self
.layout
4331 for ident
, node_type
, rna_name
in sorted(blender_mat_color_nodes_props
, key
=lambda k
: k
[2]):
4332 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4333 props
.to_type
= ident
4336 class NWSwitchMatVectorSubmenu(Menu
, NWBase
):
4337 bl_idname
= "NODE_MT_nw_switch_mat_vector_submenu"
4340 def draw(self
, context
):
4341 layout
= self
.layout
4342 for ident
, node_type
, rna_name
in sorted(blender_mat_vector_nodes_props
, key
=lambda k
: k
[2]):
4343 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4344 props
.to_type
= ident
4347 class NWSwitchMatConverterSubmenu(Menu
, NWBase
):
4348 bl_idname
= "NODE_MT_nw_switch_mat_converter_submenu"
4349 bl_label
= "Converter"
4351 def draw(self
, context
):
4352 layout
= self
.layout
4353 for ident
, node_type
, rna_name
in sorted(blender_mat_converter_nodes_props
, key
=lambda k
: k
[2]):
4354 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4355 props
.to_type
= ident
4358 class NWSwitchMatLayoutSubmenu(Menu
, NWBase
):
4359 bl_idname
= "NODE_MT_nw_switch_mat_layout_submenu"
4362 def draw(self
, context
):
4363 layout
= self
.layout
4364 for ident
, node_type
, rna_name
in sorted(blender_mat_layout_nodes_props
, key
=lambda k
: k
[2]):
4365 if node_type
!= 'FRAME':
4366 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4367 props
.to_type
= ident
4370 class NWSwitchTexInputSubmenu(Menu
, NWBase
):
4371 bl_idname
= "NODE_MT_nw_switch_tex_input_submenu"
4374 def draw(self
, context
):
4375 layout
= self
.layout
4376 for ident
, node_type
, rna_name
in sorted(texture_input_nodes_props
, key
=lambda k
: k
[2]):
4377 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4378 props
.to_type
= ident
4381 class NWSwitchTexOutputSubmenu(Menu
, NWBase
):
4382 bl_idname
= "NODE_MT_nw_switch_tex_output_submenu"
4385 def draw(self
, context
):
4386 layout
= self
.layout
4387 for ident
, node_type
, rna_name
in sorted(texture_output_nodes_props
, key
=lambda k
: k
[2]):
4388 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4389 props
.to_type
= ident
4392 class NWSwitchTexColorSubmenu(Menu
, NWBase
):
4393 bl_idname
= "NODE_MT_nw_switch_tex_color_submenu"
4396 def draw(self
, context
):
4397 layout
= self
.layout
4398 for ident
, node_type
, rna_name
in sorted(texture_color_nodes_props
, key
=lambda k
: k
[2]):
4399 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4400 props
.to_type
= ident
4403 class NWSwitchTexPatternSubmenu(Menu
, NWBase
):
4404 bl_idname
= "NODE_MT_nw_switch_tex_pattern_submenu"
4405 bl_label
= "Pattern"
4407 def draw(self
, context
):
4408 layout
= self
.layout
4409 for ident
, node_type
, rna_name
in sorted(texture_pattern_nodes_props
, key
=lambda k
: k
[2]):
4410 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4411 props
.to_type
= ident
4414 class NWSwitchTexTexturesSubmenu(Menu
, NWBase
):
4415 bl_idname
= "NODE_MT_nw_switch_tex_textures_submenu"
4416 bl_label
= "Textures"
4418 def draw(self
, context
):
4419 layout
= self
.layout
4420 for ident
, node_type
, rna_name
in sorted(texture_textures_nodes_props
, key
=lambda k
: k
[2]):
4421 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4422 props
.to_type
= ident
4425 class NWSwitchTexConverterSubmenu(Menu
, NWBase
):
4426 bl_idname
= "NODE_MT_nw_switch_tex_converter_submenu"
4427 bl_label
= "Converter"
4429 def draw(self
, context
):
4430 layout
= self
.layout
4431 for ident
, node_type
, rna_name
in sorted(texture_converter_nodes_props
, key
=lambda k
: k
[2]):
4432 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4433 props
.to_type
= ident
4436 class NWSwitchTexDistortSubmenu(Menu
, NWBase
):
4437 bl_idname
= "NODE_MT_nw_switch_tex_distort_submenu"
4438 bl_label
= "Distort"
4440 def draw(self
, context
):
4441 layout
= self
.layout
4442 for ident
, node_type
, rna_name
in sorted(texture_distort_nodes_props
, key
=lambda k
: k
[2]):
4443 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4444 props
.to_type
= ident
4447 class NWSwitchTexLayoutSubmenu(Menu
, NWBase
):
4448 bl_idname
= "NODE_MT_nw_switch_tex_layout_submenu"
4451 def draw(self
, context
):
4452 layout
= self
.layout
4453 for ident
, node_type
, rna_name
in sorted(texture_layout_nodes_props
, key
=lambda k
: k
[2]):
4454 if node_type
!= 'FRAME':
4455 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=rna_name
)
4456 props
.to_type
= ident
4460 # APPENDAGES TO EXISTING UI
4464 def select_parent_children_buttons(self
, context
):
4465 layout
= self
.layout
4466 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
4467 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
4470 def attr_nodes_menu_func(self
, context
):
4471 col
= self
.layout
.column(align
=True)
4472 col
.menu("NODE_MT_nw_node_vertex_color_menu")
4476 def multipleimages_menu_func(self
, context
):
4477 col
= self
.layout
.column(align
=True)
4478 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
4479 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
4483 def bgreset_menu_func(self
, context
):
4484 self
.layout
.operator(NWResetBG
.bl_idname
)
4487 def save_viewer_menu_func(self
, context
):
4488 if nw_check(context
):
4489 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4490 if context
.scene
.node_tree
.nodes
.active
:
4491 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
4492 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
4495 def reset_nodes_button(self
, context
):
4496 node_active
= context
.active_node
4497 node_selected
= context
.selected_nodes
4498 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4500 # Check if active node is in the selection and respective type
4501 if (len(node_selected
) == 1) and node_active
.select
and node_active
.type not in node_ignore
:
4502 row
= self
.layout
.row()
4503 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
4504 self
.layout
.separator()
4506 elif (len(node_selected
) == 1) and node_active
.select
and node_active
.type == "FRAME":
4507 row
= self
.layout
.row()
4508 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
4509 self
.layout
.separator()
4513 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4517 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4518 # props entry: (property name, property value)
4521 # NWMergeNodes with Ctrl (AUTO).
4522 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
4523 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4524 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
4525 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4526 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4527 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4528 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
4529 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4530 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4531 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4532 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
4533 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4534 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4535 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4536 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
4537 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4538 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4539 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4540 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
4541 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4542 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
4543 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4544 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
4545 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4546 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4547 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4548 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4549 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
4550 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4551 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
4552 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4553 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4554 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4555 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
4556 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4557 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4558 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4559 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
4560 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4561 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4562 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4563 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
4564 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4565 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4566 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4567 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
4568 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4569 # NWMergeNodes with Ctrl Shift (MATH)
4570 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4571 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4572 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
4573 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4574 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4575 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4576 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
4577 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4578 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4579 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4580 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
4581 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4582 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4583 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4584 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
4585 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4586 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
4587 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4588 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
4589 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4590 # BATCH CHANGE NODES
4591 # NWBatchChangeNodes with Alt
4592 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
4593 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4594 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
4595 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4596 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4597 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4598 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
4599 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4600 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4601 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4602 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
4603 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4604 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4605 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4606 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
4607 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4608 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4609 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4610 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
4611 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4612 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
4613 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4614 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
4615 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4616 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
4617 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4618 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
4619 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4620 # LINK ACTIVE TO SELECTED
4621 # Don't use names, don't replace links (K)
4622 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
4623 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4624 # Don't use names, replace links (Shift K)
4625 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
4626 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4627 # Use node name, don't replace links (')
4628 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
4629 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4630 # Use node name, replace links (Shift ')
4631 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
4632 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4633 # Don't use names, don't replace links (;)
4634 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
4635 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4636 # Don't use names, replace links (')
4637 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
4638 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4640 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4641 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4642 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4643 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4644 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4645 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4646 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4647 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4648 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4649 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4650 # CLEAR LABEL (Alt L)
4651 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4652 # MODIFY LABEL (Alt Shift L)
4653 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4654 # Copy Label from active to selected
4655 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4656 # DETACH OUTPUTS (Alt Shift D)
4657 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4658 # LINK TO OUTPUT NODE (O)
4659 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4660 # SELECT PARENT/CHILDREN
4662 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4664 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4666 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4667 # Add Principled BSDF Texture Setup
4668 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4670 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4672 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4674 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4676 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4678 (NWEmissionViewer
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4680 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
4682 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4684 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
4685 # Lazy Connect with Menu
4686 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4687 # Viewer Tile Center
4688 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4690 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4691 # Reset Nodes (Back Space)
4692 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4694 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
4695 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4696 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4697 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
4698 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
4699 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
4704 NWPrincipledPreferences
,
4724 NWAddPrincipledSetup
,
4726 NWLinkActiveToSelected
,
4728 NWSelectParentChildren
,
4734 NWAddMultipleImages
,
4743 NWConnectionListOutputs
,
4744 NWConnectionListInputs
,
4746 NWBatchChangeNodesMenu
,
4747 NWBatchChangeBlendTypeMenu
,
4748 NWBatchChangeOperationMenu
,
4749 NWCopyToSelectedMenu
,
4752 NWLinkActiveToSelectedMenu
,
4754 NWLinkUseNodeNameMenu
,
4755 NWLinkUseOutputsNamesMenu
,
4757 NWSwitchNodeTypeMenu
,
4758 NWSwitchShadersInputSubmenu
,
4759 NWSwitchShadersOutputSubmenu
,
4760 NWSwitchShadersShaderSubmenu
,
4761 NWSwitchShadersTextureSubmenu
,
4762 NWSwitchShadersColorSubmenu
,
4763 NWSwitchShadersVectorSubmenu
,
4764 NWSwitchShadersConverterSubmenu
,
4765 NWSwitchShadersLayoutSubmenu
,
4766 NWSwitchCompoInputSubmenu
,
4767 NWSwitchCompoOutputSubmenu
,
4768 NWSwitchCompoColorSubmenu
,
4769 NWSwitchCompoConverterSubmenu
,
4770 NWSwitchCompoFilterSubmenu
,
4771 NWSwitchCompoVectorSubmenu
,
4772 NWSwitchCompoMatteSubmenu
,
4773 NWSwitchCompoDistortSubmenu
,
4774 NWSwitchCompoLayoutSubmenu
,
4775 NWSwitchMatInputSubmenu
,
4776 NWSwitchMatOutputSubmenu
,
4777 NWSwitchMatColorSubmenu
,
4778 NWSwitchMatVectorSubmenu
,
4779 NWSwitchMatConverterSubmenu
,
4780 NWSwitchMatLayoutSubmenu
,
4781 NWSwitchTexInputSubmenu
,
4782 NWSwitchTexOutputSubmenu
,
4783 NWSwitchTexColorSubmenu
,
4784 NWSwitchTexPatternSubmenu
,
4785 NWSwitchTexTexturesSubmenu
,
4786 NWSwitchTexConverterSubmenu
,
4787 NWSwitchTexDistortSubmenu
,
4788 NWSwitchTexLayoutSubmenu
,
4792 from bpy
.utils
import register_class
4795 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
4796 name
="Busy Drawing!",
4798 description
="An internal property used to store only the first mouse position")
4799 bpy
.types
.Scene
.NWLazySource
= StringProperty(
4800 name
="Lazy Source!",
4802 description
="An internal property used to store the first node in a Lazy Connect operation")
4803 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
4804 name
="Lazy Target!",
4806 description
="An internal property used to store the last node in a Lazy Connect operation")
4807 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
4808 name
="Source Socket!",
4810 description
="An internal property used to store the source socket in a Lazy Connect operation")
4816 addon_keymaps
.clear()
4817 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
4819 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
4820 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
4821 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
4823 for prop
, value
in props
:
4824 setattr(kmi
.properties
, prop
, value
)
4825 addon_keymaps
.append((km
, kmi
))
4828 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
4829 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
4830 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
4831 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
4832 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
4833 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
4834 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
4835 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
4839 from bpy
.utils
import unregister_class
4842 del bpy
.types
.Scene
.NWBusyDrawing
4843 del bpy
.types
.Scene
.NWLazySource
4844 del bpy
.types
.Scene
.NWLazyTarget
4845 del bpy
.types
.Scene
.NWSourceSocket
4848 for km
, kmi
in addon_keymaps
:
4849 km
.keymap_items
.remove(kmi
)
4850 addon_keymaps
.clear()
4853 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
4854 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
4855 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
4856 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
4857 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
4858 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
4859 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
4860 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
4863 unregister_class(cls
)
4865 if __name__
== "__main__":