sun_position: fix warning from deleted prop in User Preferences
[blender-addons.git] / node_wrangler.py
blob0fad2ccad6d62d2f43e451c478fce76f676a7b94
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Node Wrangler",
21 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
22 "version": (3, 36),
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",
26 "warning": "",
27 "wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
28 "node/node_wrangler.html",
29 "category": "Node",
32 import bpy, blf, bgl
33 import gpu
34 from bpy.types import Operator, Panel, Menu
35 from bpy.props import (
36 FloatProperty,
37 EnumProperty,
38 BoolProperty,
39 IntProperty,
40 StringProperty,
41 FloatVectorProperty,
42 CollectionProperty,
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
48 from os import path
49 from glob import glob
50 from copy import copy
51 from itertools import chain
52 import re
53 from collections import namedtuple
55 #################
56 # rl_outputs:
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'])
63 rl_outputs = (
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),
91 # shader nodes
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'),
202 # compositing nodes
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'),
383 # Texture Nodes
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.
460 blend_types = [
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.
483 operations = [
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.
507 navs = [
508 ('CURRENT', 'Current', 'Leave at current state'),
509 ('NEXT', 'Next', 'Next blend type/operation'),
510 ('PREV', 'Prev', 'Previous blend type/operation'),
513 draw_color_sets = {
514 "red_white": (
515 (1.0, 1.0, 1.0, 0.7),
516 (1.0, 0.0, 0.0, 0.7),
517 (0.8, 0.2, 0.2, 1.0)
519 "green": (
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)
524 "yellow": (
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)
529 "purple": (
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)
534 "grey": (
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)
539 "black": (
540 (1.0, 1.0, 1.0, 0.7),
541 (0.0, 0.0, 0.0, 0.7),
542 (0.2, 0.2, 0.2, 1.0)
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
555 pairs = (
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"),
563 ('ZERO', "0"),
564 ('ONE', "1"),
565 ('TWO', "2"),
566 ('THREE', "3"),
567 ('FOUR', "4"),
568 ('FIVE', "5"),
569 ('SIX', "6"),
570 ('SEVEN', "7"),
571 ('EIGHT', "8"),
572 ('NINE', "9"),
573 ('OSKEY', "Super"),
574 ('RET', "Enter"),
575 ('LINE_FEED', "Enter"),
576 ('SEMI_COLON', ";"),
577 ('PERIOD', "."),
578 ('COMMA', ","),
579 ('QUOTE', '"'),
580 ('MINUS', "-"),
581 ('SLASH', "/"),
582 ('BACK_SLASH', "\\"),
583 ('EQUAL', "="),
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 +"),
601 nice_punc = False
602 for (ugly, nice) in pairs:
603 if punc == ugly:
604 nice_punc = nice
605 break
606 if not nice_punc:
607 nice_punc = punc.replace("_", " ").title()
608 return nice_punc
611 def force_update(context):
612 context.space_data.node_tree.update_tag()
615 def dpifac():
616 prefs = bpy.context.preferences.system
617 return prefs.dpi * prefs.pixel_size / 72
620 def node_mid_pt(node, axis):
621 if axis == 'x':
622 d = node.location.x + (node.dimensions.x / 2)
623 elif axis == 'y':
624 d = node.location.y - (node.dimensions.y / 2)
625 else:
626 d = 0
627 return d
630 def autolink(node1, node2, links):
631 link_made = False
633 for outp in node1.outputs:
634 for inp in node2.inputs:
635 if not inp.is_linked and inp.name == outp.name:
636 link_made = True
637 links.new(outp, inp)
638 return True
640 for outp in node1.outputs:
641 for inp in node2.inputs:
642 if not inp.is_linked and inp.type == outp.type:
643 link_made = True
644 links.new(outp, inp)
645 return True
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:
651 link_made = True
652 links.new(outp, inp)
653 return True
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:
659 link_made = True
660 links.new(outp, inp)
661 return True
663 # do something!
664 for outp in node1.outputs:
665 for inp in node2.inputs:
666 link_made = True
667 links.new(outp, inp)
668 return True
670 print("Could not make a link from " + node1.name + " to " + node2.name)
671 return link_made
674 def node_at_pos(nodes, context, event):
675 nodes_near_mouse = []
676 nodes_under_mouse = []
677 target_node = None
679 store_mouse_cursor(context, event)
680 x, y = context.space_data.cursor_location
681 x = x
682 y = y
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 = []
687 for node in nodes:
688 skipnode = False
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()
694 if node.parent:
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...
706 skipnode = True
707 if not skipnode:
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]
720 for node in nodes:
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()
726 if node.parent:
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
736 else:
737 target_node = nearest_node # else use the nearest node
738 else:
739 target_node = nearest_node
740 return target_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)
751 else:
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),
762 colour)
764 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
765 bgl.glLineWidth(size * dpifac())
767 shader.bind()
768 batch.draw(shader)
771 def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
772 radius = radius * dpifac()
773 sides = 12
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})
779 shader.bind()
780 shader.uniform_float("color", colour)
781 batch.draw(shader)
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
786 sides = 16
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
794 if node.parent:
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
804 if node.hide:
805 nlocx += -1
806 nlocy += 5
807 if node.type == 'REROUTE':
808 #nlocx += 1
809 nlocy -= 1
810 ndimx = 0
811 ndimy = 0
812 radius += 6
814 # Top left corner
815 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
816 vertices = [(mx,my)]
817 for i in range(sides+1):
818 if (4<=i<=8):
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})
824 shader.bind()
825 shader.uniform_float("color", colour)
826 batch.draw(shader)
828 # Top right corner
829 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
830 vertices = [(mx,my)]
831 for i in range(sides+1):
832 if (0<=i<=4):
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})
838 shader.bind()
839 shader.uniform_float("color", colour)
840 batch.draw(shader)
842 # Bottom left corner
843 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
844 vertices = [(mx,my)]
845 for i in range(sides+1):
846 if (8<=i<=12):
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})
852 shader.bind()
853 shader.uniform_float("color", colour)
854 batch.draw(shader)
856 # Bottom right corner
857 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
858 vertices = [(mx,my)]
859 for i in range(sides+1):
860 if (12<=i<=16):
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})
866 shader.bind()
867 shader.uniform_float("color", colour)
868 batch.draw(shader)
870 # prepare drawing all edges in one batch
871 vertices = []
872 indices = []
873 id_last = 0
875 # Left edge
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)])
883 id_last += 4
885 # Top edge
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)])
895 id_last += 4
897 # Right edge
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)])
907 id_last += 4
909 # Bottom edge
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)
923 shader.bind()
924 shader.uniform_float("color", colour)
925 batch.draw(shader)
927 def draw_callback_nodeoutline(self, context, mode):
928 if self.mouse_path:
930 bgl.glLineWidth(1)
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')
939 if mode == "LINK":
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)
947 elif mode == "MIX":
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]
960 if n1 == n2:
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
973 # circle outline
974 draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
975 draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
977 # circle inner
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
998 # Principled prefs
999 class NWPrincipledPreferences(bpy.types.PropertyGroup):
1000 base_color: StringProperty(
1001 name='Base Color',
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(
1009 name='Metallic',
1010 default='metallic metalness metal mtl',
1011 description='Naming Components for metallness maps')
1012 specular: StringProperty(
1013 name='Specular',
1014 default='specularity specular spec spc',
1015 description='Naming Components for Specular maps')
1016 normal: StringProperty(
1017 name='Normal',
1018 default='normal nor nrm nrml norm',
1019 description='Naming Components for Normal maps')
1020 bump: StringProperty(
1021 name='Bump',
1022 default='bump bmp',
1023 description='Naming Components for bump maps')
1024 rough: StringProperty(
1025 name='Roughness',
1026 default='roughness rough rgh',
1027 description='Naming Components for roughness maps')
1028 gloss: StringProperty(
1029 name='Gloss',
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')
1037 # Addon prefs
1038 class NWNodeWrangler(bpy.types.AddonPreferences):
1039 bl_idname = __name__
1041 merge_hide: EnumProperty(
1042 name="Hide Mix nodes",
1043 items=(
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",
1052 items=(
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")
1056 default='CENTER',
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",
1061 default=False,
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",
1066 default="",
1067 description="Show only hotkeys that have this text in their name"
1069 show_principled_lists: BoolProperty(
1070 name="Show Principled naming tags",
1071 default=False,
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")
1082 box = layout.box()
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")
1098 box = layout.box()
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")
1106 col.separator()
1107 for hotkey in kmi_defs:
1108 if hotkey[7]:
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])
1115 if hotkey[4]:
1116 keystr = "Shift " + keystr
1117 if hotkey[5]:
1118 keystr = "Alt " + keystr
1119 if hotkey[3]:
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"]
1129 valid = False
1130 if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
1131 valid = True
1133 return valid
1135 class NWBase:
1136 @classmethod
1137 def poll(cls, context):
1138 return nw_check(context)
1141 # OPERATORS
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)
1151 cont = True
1153 start_pos = [event.mouse_region_x, event.mouse_region_y]
1155 node1 = None
1156 if not context.scene.NWBusyDrawing:
1157 node1 = node_at_pos(nodes, context, event)
1158 if node1:
1159 context.scene.NWBusyDrawing = node1.name
1160 else:
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')
1174 node2 = None
1175 node2 = node_at_pos(nodes, context, event)
1176 if node2:
1177 context.scene.NWBusyDrawing = node2.name
1179 if node1 == node2:
1180 cont = False
1182 if cont:
1183 if node1 and node2:
1184 for node in nodes:
1185 node.select = False
1186 node1.select = True
1187 node2.select = True
1189 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
1191 context.scene.NWBusyDrawing = ""
1192 return {'FINISHED'}
1194 elif event.type == 'ESC':
1195 print('cancelled')
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'}
1213 else:
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)
1228 cont = True
1230 start_pos = [event.mouse_region_x, event.mouse_region_y]
1232 node1 = None
1233 if not context.scene.NWBusyDrawing:
1234 node1 = node_at_pos(nodes, context, event)
1235 if node1:
1236 context.scene.NWBusyDrawing = node1.name
1237 else:
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')
1251 node2 = None
1252 node2 = node_at_pos(nodes, context, event)
1253 if node2:
1254 context.scene.NWBusyDrawing = node2.name
1256 if node1 == node2:
1257 cont = False
1259 link_success = False
1260 if cont:
1261 if node1 and node2:
1262 original_sel = []
1263 original_unsel = []
1264 for node in nodes:
1265 if node.select == True:
1266 node.select = False
1267 original_sel.append(node)
1268 else:
1269 original_unsel.append(node)
1270 node1.select = True
1271 node2.select = True
1273 #link_success = autolink(node1, node2, links)
1274 if self.with_menu:
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)
1279 else:
1280 link_success = autolink(node1, node2, links)
1282 for node in original_sel:
1283 node.select = True
1284 for node in original_unsel:
1285 node.select = False
1287 if link_success:
1288 force_update(context)
1289 context.scene.NWBusyDrawing = ""
1290 return {'FINISHED'}
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)
1302 if node:
1303 context.scene.NWBusyDrawing = node.name
1305 # the arguments we pass the the callback
1306 mode = "LINK"
1307 if self.with_menu:
1308 mode = "LINKMENU"
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'}
1318 else:
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:
1337 return False
1339 for output in node.outputs:
1340 if output.links:
1341 return False
1342 return True
1344 @classmethod
1345 def poll(cls, context):
1346 valid = False
1347 if nw_check(context):
1348 if context.space_data.node_tree.nodes:
1349 valid = True
1350 return valid
1352 def execute(self, context):
1353 nodes, links = get_nodes_links(context)
1355 # Store selection
1356 selection = []
1357 for node in nodes:
1358 if node.select == True:
1359 selection.append(node.name)
1361 for node in nodes:
1362 node.select = False
1364 deleted_nodes = []
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
1369 for node in nodes:
1370 if self.is_unused_node(node):
1371 node.select = True
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
1376 break
1378 if self.delete_frames:
1379 repeat = True
1380 while repeat:
1381 frames_in_use = []
1382 frames = []
1383 repeat = False
1384 for node in nodes:
1385 if node.parent:
1386 frames_in_use.append(node.parent)
1387 for node in nodes:
1388 if node.type == 'FRAME' and node not in frames_in_use:
1389 frames.append(node)
1390 if node.parent:
1391 repeat = True # repeat for nested frames
1392 for node in frames:
1393 if node not in frames_in_use:
1394 node.select = True
1395 deleted_nodes.append(node.name)
1396 bpy.ops.node.delete()
1398 if self.delete_muted:
1399 for node in nodes:
1400 if node.mute:
1401 node.select = True
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)
1410 n = ' node'
1411 if num_deleted > 1:
1412 n += 's'
1413 if num_deleted:
1414 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1415 else:
1416 self.report({'INFO'}, "Nothing deleted")
1418 # Restore selection
1419 nodes, links = get_nodes_links(context)
1420 for node in nodes:
1421 if node.name in selection:
1422 node.select = True
1423 return {'FINISHED'}
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'}
1435 @classmethod
1436 def poll(cls, context):
1437 valid = False
1438 if nw_check(context):
1439 if context.selected_nodes:
1440 valid = len(context.selected_nodes) <= 2
1441 return valid
1443 def execute(self, context):
1444 nodes, links = get_nodes_links(context)
1445 selected_nodes = context.selected_nodes
1446 n1 = selected_nodes[0]
1448 # Swap outputs
1449 if len(selected_nodes) == 2:
1450 n2 = selected_nodes[1]
1451 if n1.outputs and n2.outputs:
1452 n1_outputs = []
1453 n2_outputs = []
1455 out_index = 0
1456 for output in n1.outputs:
1457 if output.links:
1458 for link in output.links:
1459 n1_outputs.append([out_index, link.to_socket])
1460 links.remove(link)
1461 out_index += 1
1463 out_index = 0
1464 for output in n2.outputs:
1465 if output.links:
1466 for link in output.links:
1467 n2_outputs.append([out_index, link.to_socket])
1468 links.remove(link)
1469 out_index += 1
1471 for connection in n1_outputs:
1472 try:
1473 links.new(n2.outputs[connection[0]], connection[1])
1474 except:
1475 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1476 for connection in n2_outputs:
1477 try:
1478 links.new(n1.outputs[connection[0]], connection[1])
1479 except:
1480 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1481 else:
1482 if n1.outputs or n2.outputs:
1483 self.report({'WARNING'}, "One of the nodes has no outputs!")
1484 else:
1485 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1487 # Swap Inputs
1488 elif len(selected_nodes) == 1:
1489 if n1.inputs:
1490 types = []
1492 for i1 in n1.inputs:
1493 if i1.is_linked:
1494 similar_types = 0
1495 for i2 in n1.inputs:
1496 if i1.type == i2.type and i2.is_linked:
1497 similar_types += 1
1498 types.append ([i1, similar_types, i])
1499 i += 1
1500 types.sort(key=lambda k: k[1], reverse=True)
1502 if types:
1503 t = types[0]
1504 if t[1] == 2:
1505 for i2 in n1.inputs:
1506 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1507 pair = [t[0], i2]
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
1512 links.new(i1f, i2t)
1513 links.new(i2f, i1t)
1514 if t[1] == 1:
1515 if len(types) == 1:
1516 fs = t[0].links[0].from_socket
1517 i = t[2]
1518 links.remove(t[0].links[0])
1519 if i+1 == len(n1.inputs):
1520 i = -1
1521 i += 1
1522 while n1.inputs[i].is_linked:
1523 i += 1
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
1530 links.new(i1f, i2t)
1531 links.new(i2f, i1t)
1533 else:
1534 self.report({'WARNING'}, "This node has no input connections to swap!")
1535 else:
1536 self.report({'WARNING'}, "This node has no inputs to swap!")
1538 force_update(context)
1539 return {'FINISHED'}
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'}
1548 @classmethod
1549 def poll(cls, context):
1550 valid = False
1551 if nw_check(context):
1552 snode = context.space_data
1553 valid = snode.tree_type == 'CompositorNodeTree'
1554 return valid
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
1560 return {'FINISHED'}
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
1575 return {'FINISHED'}
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'}
1584 @classmethod
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":
1592 return True
1593 else:
1594 return True
1595 return False
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"
1605 else:
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]
1623 valid = False
1624 if active:
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):
1628 valid = True
1629 break
1630 if valid:
1631 # get material_output node, store selection, deselect all
1632 materialout = None # placeholder node
1633 selection = []
1634 for node in nodes:
1635 if node.type == shader_output_type:
1636 materialout = node
1637 if node.select:
1638 selection.append(node.name)
1639 node.select = False
1640 if not materialout:
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
1648 sum_yloc = 0
1649 for node in nodes:
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
1660 out_i = None
1661 valid_outputs = []
1662 for i, out in enumerate(active.outputs):
1663 if is_visible_socket(out):
1664 valid_outputs.append(i)
1665 if valid_outputs:
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]
1672 else:
1673 out_i = valid_outputs[0]
1674 make_links = [] # store sockets for new links
1675 if active.outputs:
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]
1681 for node in nodes:
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
1694 else:
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]))
1702 else:
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
1708 else:
1709 intensity = 1
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
1714 else:
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]))
1717 for node in nodes:
1718 if node.name == 'Emission Viewer':
1719 node.select = True
1720 bpy.ops.node.delete()
1721 for li_from, li_to in make_links:
1722 links.new(li_from, li_to)
1723 # Restore selection
1724 nodes.active = active
1725 for node in nodes:
1726 if node.name in selection:
1727 node.select = True
1728 force_update(context)
1729 return {'FINISHED'}
1730 else:
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(
1741 name='Label',
1742 description='The visual name of the frame node',
1743 default=' '
1745 color_prop: FloatVectorProperty(
1746 name="Color",
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)
1755 selected = []
1756 for node in nodes:
1757 if node.select == True:
1758 selected.append(node)
1760 bpy.ops.node.add_node(type='NodeFrame')
1761 frm = nodes.active
1762 frm.label = self.label_prop
1763 frm.use_custom_color = True
1764 frm.color = self.color_prop
1766 for node in selected:
1767 node.parent = frm
1769 return {'FINISHED'}
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"]
1780 num_reloaded = 0
1781 for node in nodes:
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()
1788 num_reloaded += 1
1789 else:
1790 if node.image:
1791 node.image.reload()
1792 num_reloaded += 1
1794 if num_reloaded:
1795 self.report({'INFO'}, "Reloaded images")
1796 print("Reloaded " + str(num_reloaded) + " images")
1797 force_update(context)
1798 return {'FINISHED'}
1799 else:
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]
1856 reselect = []
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'):
1866 if node.image:
1867 new_node.image = node.image
1868 # Special cases
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.
1876 # socket entry:
1877 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1878 src_sockets = {
1879 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1880 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1882 dst_sockets = {
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
1896 dval = None
1897 # Not every socket, especially in outputs has "default_value"
1898 if hasattr(socket, 'default_value'):
1899 dval = socket.default_value
1900 socket_links = []
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
1914 break
1916 matches = {
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']]
1937 else:
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.
1943 if len(sc) > i:
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':
1947 for s in sc:
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]:
1961 # pass dvals
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\
1967 tp == 'MAIN':
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\
1976 tp == 'MAIN':
1977 for i in range(3):
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)
2013 nodes.remove(node)
2014 force_update(context)
2015 return {'FINISHED'}
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'}
2024 mode: EnumProperty(
2025 name="mode",
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(
2030 name="merge type",
2031 description="Type of Merge to be used",
2032 items=(
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'
2047 do_hide = False
2048 do_hide_shader = False
2049 if merge_hide == 'ALWAYS':
2050 do_hide = True
2051 do_hide_shader = True
2052 elif merge_hide == 'NON_SHADER':
2053 do_hide = True
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)
2063 mode = self.mode
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':
2069 merge_type = 'MIX'
2070 mode = 'MIX'
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'
2094 valid_mode = True
2095 if output_type == type and valid_mode:
2096 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2097 else:
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
2112 selected_math = []
2114 for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
2115 if nodes_list:
2116 count_before = len(nodes)
2117 # sort list by loc_x - reversed
2118 nodes_list.sort(key=lambda k: k[1], reverse=True)
2119 # get maximum loc_x
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
2125 if do_hide:
2126 loc_y += 40
2127 else:
2128 loc_y += 80
2129 else:
2130 loc_y = nodes_list[len(nodes_list) - 1][2]
2131 offset_y = 100
2132 if not do_hide:
2133 offset_y = 200
2134 if nodes_list == selected_shader and not do_hide_shader:
2135 offset_y = 150.0
2136 the_range = len(nodes_list) - 1
2137 if len(nodes_list) == 1:
2138 the_range = 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
2144 if mode != 'MIX':
2145 add.inputs[0].default_value = 1.0
2146 add.show_preview = False
2147 add.hide = do_hide
2148 if do_hide:
2149 loc_y = loc_y - 50
2150 first = 1
2151 second = 2
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
2157 add.hide = do_hide
2158 if do_hide:
2159 loc_y = loc_y - 50
2160 first = 0
2161 second = 1
2162 add.width_hidden = 100.0
2163 elif nodes_list == selected_shader:
2164 if mode == 'MIX':
2165 add_type = node_type + 'MixShader'
2166 add = nodes.new(add_type)
2167 add.hide = do_hide_shader
2168 if do_hide_shader:
2169 loc_y = loc_y - 50
2170 first = 1
2171 second = 2
2172 add.width_hidden = 100.0
2173 elif mode == 'ADD':
2174 add_type = node_type + 'AddShader'
2175 add = nodes.new(add_type)
2176 add.hide = do_hide_shader
2177 if do_hide_shader:
2178 loc_y = loc_y - 50
2179 first = 0
2180 second = 1
2181 add.width_hidden = 100.0
2182 elif nodes_list == selected_z:
2183 add = nodes.new('CompositorNodeZcombine')
2184 add.show_preview = False
2185 add.hide = do_hide
2186 if do_hide:
2187 loc_y = loc_y - 50
2188 first = 0
2189 second = 2
2190 add.width_hidden = 100.0
2191 elif nodes_list == selected_alphaover:
2192 add = nodes.new('CompositorNodeAlphaOver')
2193 add.show_preview = False
2194 add.hide = do_hide
2195 if do_hide:
2196 loc_y = loc_y - 50
2197 first = 1
2198 second = 2
2199 add.width_hidden = 100.0
2200 add.location = loc_x, loc_y
2201 loc_y += offset_y
2202 add.select = True
2203 count_adds = i + 1
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]
2209 # Special case:
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])
2237 break
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])
2260 index -= 1
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
2266 return {'FINISHED'}
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(
2276 name="Blend Type",
2277 items=blend_types + navs,
2279 operation: EnumProperty(
2280 name="Operation",
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
2293 else:
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]
2299 else:
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]
2304 if index == 0:
2305 node.blend_type = blend_types[len(blend_types) - 1][0]
2306 else:
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
2312 else:
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]
2318 else:
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)
2324 if index == 0:
2325 node.operation = operations[len(operations) - 1][0]
2326 else:
2327 node.operation = operations[index - 1][0]
2329 return {'FINISHED'}
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):
2348 if node.select:
2349 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2350 selected.append(si)
2352 for si in selected:
2353 fac = nodes[si].inputs[0]
2354 nodes[si].hide = False
2355 if option in {0.0, 1.0}:
2356 fac.default_value = option
2357 else:
2358 fac.default_value += option
2360 return {'FINISHED'}
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'}
2369 @classmethod
2370 def poll(cls, context):
2371 valid = False
2372 if nw_check(context):
2373 if (
2374 context.active_node is not None and
2375 context.active_node.type != 'FRAME'
2377 valid = True
2378 return valid
2380 def execute(self, context):
2381 node_active = context.active_node
2382 node_selected = context.selected_nodes
2384 # Error handling
2385 if not (len(node_selected) > 1):
2386 self.report({'ERROR'}, "2 nodes must be selected at least")
2387 return {'CANCELLED'}
2389 # Check if active node is in the selection
2390 selected_node_names = [n.name for n in node_selected]
2391 if node_active.name not in selected_node_names:
2392 self.report({'ERROR'}, "No active node")
2393 return {'CANCELLED'}
2395 # Get nodes in selection by type
2396 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2398 if not (len(valid_nodes) > 1) and node_active:
2399 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2400 return {'CANCELLED'}
2402 if len(valid_nodes) != len(node_selected):
2403 # Report nodes that are not valid
2404 valid_node_names = [n.name for n in valid_nodes]
2405 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2406 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2408 # Reference original
2409 orig = node_active
2410 #node_selected_names = [n.name for n in node_selected]
2412 # Output list
2413 success_names = []
2415 # Deselect all nodes
2416 for i in node_selected:
2417 i.select = False
2419 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2420 # Run through all other nodes
2421 for node in valid_nodes[1:]:
2423 # Check for frame node
2424 parent = node.parent if node.parent else None
2425 node_loc = [node.location.x, node.location.y]
2427 # Select original to duplicate
2428 orig.select = True
2430 # Duplicate selected node
2431 bpy.ops.node.duplicate()
2432 new_node = context.selected_nodes[0]
2434 # Deselect copy
2435 new_node.select = False
2437 # Properties to copy
2438 node_tree = node.id_data
2439 props_to_copy = 'bl_idname name location height width'.split(' ')
2441 # Input and outputs
2442 reconnections = []
2443 mappings = chain.from_iterable([node.inputs, node.outputs])
2444 for i in (i for i in mappings if i.is_linked):
2445 for L in i.links:
2446 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2448 # Properties
2449 props = {j: getattr(node, j) for j in props_to_copy}
2450 props_to_copy.pop(0)
2452 for prop in props_to_copy:
2453 setattr(new_node, prop, props[prop])
2455 # Get the node tree to remove the old node
2456 nodes = node_tree.nodes
2457 nodes.remove(node)
2458 new_node.name = props['name']
2460 if parent:
2461 new_node.parent = parent
2462 new_node.location = node_loc
2464 for str_from, str_to in reconnections:
2465 node_tree.links.new(eval(str_from), eval(str_to))
2467 success_names.append(new_node.name)
2469 orig.select = True
2470 node_tree.nodes.active = orig
2471 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2472 return {'FINISHED'}
2475 class NWCopyLabel(Operator, NWBase):
2476 bl_idname = "node.nw_copy_label"
2477 bl_label = "Copy Label"
2478 bl_options = {'REGISTER', 'UNDO'}
2480 option: EnumProperty(
2481 name="option",
2482 description="Source of name of label",
2483 items=(
2484 ('FROM_ACTIVE', 'from active', 'from active node',),
2485 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2486 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2490 def execute(self, context):
2491 nodes, links = get_nodes_links(context)
2492 option = self.option
2493 active = nodes.active
2494 if option == 'FROM_ACTIVE':
2495 if active:
2496 src_label = active.label
2497 for node in [n for n in nodes if n.select and nodes.active != n]:
2498 node.label = src_label
2499 elif option == 'FROM_NODE':
2500 selected = [n for n in nodes if n.select]
2501 for node in selected:
2502 for input in node.inputs:
2503 if input.links:
2504 src = input.links[0].from_node
2505 node.label = src.label
2506 break
2507 elif option == 'FROM_SOCKET':
2508 selected = [n for n in nodes if n.select]
2509 for node in selected:
2510 for input in node.inputs:
2511 if input.links:
2512 src = input.links[0].from_socket
2513 node.label = src.name
2514 break
2516 return {'FINISHED'}
2519 class NWClearLabel(Operator, NWBase):
2520 bl_idname = "node.nw_clear_label"
2521 bl_label = "Clear Label"
2522 bl_options = {'REGISTER', 'UNDO'}
2524 option: BoolProperty()
2526 def execute(self, context):
2527 nodes, links = get_nodes_links(context)
2528 for node in [n for n in nodes if n.select]:
2529 node.label = ''
2531 return {'FINISHED'}
2533 def invoke(self, context, event):
2534 if self.option:
2535 return self.execute(context)
2536 else:
2537 return context.window_manager.invoke_confirm(self, event)
2540 class NWModifyLabels(Operator, NWBase):
2541 """Modify Labels of all selected nodes"""
2542 bl_idname = "node.nw_modify_labels"
2543 bl_label = "Modify Labels"
2544 bl_options = {'REGISTER', 'UNDO'}
2546 prepend: StringProperty(
2547 name="Add to Beginning"
2549 append: StringProperty(
2550 name="Add to End"
2552 replace_from: StringProperty(
2553 name="Text to Replace"
2555 replace_to: StringProperty(
2556 name="Replace with"
2559 def execute(self, context):
2560 nodes, links = get_nodes_links(context)
2561 for node in [n for n in nodes if n.select]:
2562 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2564 return {'FINISHED'}
2566 def invoke(self, context, event):
2567 self.prepend = ""
2568 self.append = ""
2569 self.remove = ""
2570 return context.window_manager.invoke_props_dialog(self)
2573 class NWAddTextureSetup(Operator, NWBase):
2574 bl_idname = "node.nw_add_texture"
2575 bl_label = "Texture Setup"
2576 bl_description = "Add Texture Node Setup to Selected Shaders"
2577 bl_options = {'REGISTER', 'UNDO'}
2579 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2581 @classmethod
2582 def poll(cls, context):
2583 valid = False
2584 if nw_check(context):
2585 space = context.space_data
2586 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2587 valid = True
2588 return valid
2590 def execute(self, context):
2591 nodes, links = get_nodes_links(context)
2592 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2593 texture_types = [x[1] for x in shaders_texture_nodes_props]
2594 selected_nodes = [n for n in nodes if n.select]
2595 for t_node in selected_nodes:
2596 valid = False
2597 input_index = 0
2598 if t_node.inputs:
2599 for index, i in enumerate(t_node.inputs):
2600 if not i.is_linked:
2601 valid = True
2602 input_index = index
2603 break
2604 if valid:
2605 locx = t_node.location.x
2606 locy = t_node.location.y - t_node.dimensions.y/2
2608 xoffset = [500, 700]
2609 is_texture = False
2610 if t_node.type in texture_types + ['MAPPING']:
2611 xoffset = [290, 500]
2612 is_texture = True
2614 coordout = 2
2615 image_type = 'ShaderNodeTexImage'
2617 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
2618 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2619 if t_node.type == 'BACKGROUND':
2620 image_type = 'ShaderNodeTexEnvironment'
2622 if not is_texture:
2623 tex = nodes.new(image_type)
2624 tex.location = [locx - 200, locy + 112]
2625 nodes.active = tex
2626 links.new(tex.outputs[0], t_node.inputs[input_index])
2628 t_node.select = False
2629 if self.add_mapping or is_texture:
2630 if t_node.type != 'MAPPING':
2631 m = nodes.new('ShaderNodeMapping')
2632 m.location = [locx - xoffset[0], locy + 141]
2633 m.width = 240
2634 else:
2635 m = t_node
2636 coord = nodes.new('ShaderNodeTexCoord')
2637 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
2639 if not is_texture:
2640 links.new(m.outputs[0], tex.inputs[0])
2641 links.new(coord.outputs[coordout], m.inputs[0])
2642 else:
2643 nodes.active = m
2644 links.new(m.outputs[0], t_node.inputs[input_index])
2645 links.new(coord.outputs[coordout], m.inputs[0])
2646 else:
2647 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
2648 return {'FINISHED'}
2651 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2652 bl_idname = "node.nw_add_textures_for_principled"
2653 bl_label = "Principled Texture Setup"
2654 bl_description = "Add Texture Node Setup for Principled BSDF"
2655 bl_options = {'REGISTER', 'UNDO'}
2657 directory: StringProperty(
2658 name='Directory',
2659 subtype='DIR_PATH',
2660 default='',
2661 description='Folder to search in for image files'
2663 files: CollectionProperty(
2664 type=bpy.types.OperatorFileListElement,
2665 options={'HIDDEN', 'SKIP_SAVE'}
2668 relative_path: BoolProperty(
2669 name='Relative Path',
2670 description='Select the file relative to the blend file',
2671 default=True
2674 order = [
2675 "filepath",
2676 "files",
2679 def draw(self, context):
2680 layout = self.layout
2681 layout.alignment = 'LEFT'
2683 layout.prop(self, 'relative_path')
2685 @classmethod
2686 def poll(cls, context):
2687 valid = False
2688 if nw_check(context):
2689 space = context.space_data
2690 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2691 valid = True
2692 return valid
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'}
2709 # Helper_functions
2710 def split_into__components(fname):
2711 # Split filename into components
2712 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2713 # Remove extension
2714 fname = path.splitext(fname)[0]
2715 # Remove digits
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]
2726 return 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(' ')
2735 socketnames = [
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:
2749 fname = file.name
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)
2753 if matches:
2754 sname[2] = fname
2755 break
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])]
2761 if not socketnames:
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)
2771 else:
2772 self.report({'WARNING'}, 'Relative paths cannot be used with unsaved scenes!')
2773 print('Relative paths cannot be used with unsaved scenes!')
2775 # Add found images
2776 print('\nMatched Textures:')
2777 texture_nodes = []
2778 disp_texture = None
2779 normal_node = None
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
2801 # Find output node
2802 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2803 if output_node:
2804 if not output_node[0].inputs[2].is_linked:
2805 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
2807 continue
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
2815 # NORMAL NODES
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))
2821 if match_normal:
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])
2825 elif match_bump:
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))
2839 if match_rough:
2840 # If Roughness nothing to to
2841 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2843 elif match_gloss:
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
2851 else:
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
2859 else:
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]
2867 if disp_texture:
2868 texture_nodes.append(disp_texture)
2870 # Alignment
2871 for i, texture_node in enumerate(texture_nodes):
2872 offset = Vector((-550, (i * -280) + 200))
2873 texture_node.location = active_node.location + offset
2875 if normal_node:
2876 # Extra alignment if normal node was added
2877 normal_node.location = normal_node_texture.location + Vector((300, 0))
2879 if roughness_node:
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])
2895 else:
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
2908 frame.update()
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
2915 frame.update()
2917 # Just to be sure
2918 active_node.select = False
2919 nodes.update()
2920 links.update()
2921 force_update(context)
2922 return {'FINISHED'}
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(
2933 name="option",
2934 items=[
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
2946 valid = False
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]:
2950 if node.outputs:
2951 x = node.location.x
2952 y = node.location.y
2953 width = node.width
2954 # unhide 'REROUTE' nodes to avoid issues with location.y
2955 if node.type == 'REROUTE':
2956 node.hide = False
2957 # When node is hidden - width_hidden not usable.
2958 # Hack needed to calculate real width
2959 if node.hide:
2960 bpy.ops.node.select_all(action='DESELECT')
2961 helper = nodes.new('NodeReroute')
2962 helper.select = True
2963 node.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
2969 # delete helper
2970 node.select = False
2971 # only helper is selected now
2972 bpy.ops.node.delete()
2973 x = node.location.x + width + 20.0
2974 if node.type != 'REROUTE':
2975 y -= 35.0
2976 y_offset = -22.0
2977 loc = x, y
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':
2983 pass_used = True
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':
2989 pass_used = True
2990 else:
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)
2995 break
2996 if pass_used:
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.
3001 if valid:
3002 n = nodes.new('NodeReroute')
3003 nodes.active = n
3004 for link in output.links:
3005 links.new(n.outputs[0], link.to_socket)
3006 links.new(output, n.inputs[0])
3007 n.location = loc
3008 post_select.append(n)
3009 reroutes_count += 1
3010 y += y_offset
3011 loc = x, y
3012 # disselect the node so that after execution of script only newly created nodes are selected
3013 node.select = False
3014 # nicer reroutes distribution along y when node.hide
3015 if 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:
3020 node.select = True
3022 return {'FINISHED'}
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()
3035 @classmethod
3036 def poll(cls, context):
3037 valid = False
3038 if nw_check(context):
3039 if context.active_node is not None:
3040 if context.active_node.select:
3041 valid = True
3042 return valid
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':
3054 outputs.append(out)
3055 else:
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.
3065 pass_used = True
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)
3069 break
3070 if pass_used:
3071 outputs.append(out)
3072 doit = True # Will be changed to False when links successfully added to previous output.
3073 for out in outputs:
3074 if doit:
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
3078 if node.label:
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.
3082 if use_node_name:
3083 # Set src_name to source node name or label
3084 src_name = active.name
3085 if active.label:
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:
3093 valid = False
3094 if valid:
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:
3100 doit = False
3101 break
3103 return {'FINISHED'}
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
3117 selection = []
3118 for node in nodes:
3119 if node.select and node.type != 'FRAME':
3120 selection.append(node)
3122 # If no nodes are selected, align all nodes
3123 active_loc = None
3124 if not selection:
3125 selection = 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
3139 if horizontal:
3140 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3141 else:
3142 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3144 # Alignment
3145 current_pos = 0
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
3150 if horizontal:
3151 node.location.x = current_pos
3152 current_pos += current_margin + node.dimensions.x
3153 node.location.y = mid_y + (node.dimensions.y / 2)
3154 else:
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:
3168 if horizontal:
3169 node.location.x += (mid_x - new_mid)
3170 else:
3171 node.location.y += (mid_y - new_mid)
3173 return {'FINISHED'}
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(
3182 name="option",
3183 items=(
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:
3195 parent = sel.parent
3196 if parent:
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:
3202 kid.select = True
3204 return {'FINISHED'}
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:
3220 node.select = True
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')
3226 return {'FINISHED'}
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'}
3235 @classmethod
3236 def poll(cls, context):
3237 valid = False
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):
3242 valid = True
3243 break
3244 return valid
3246 def execute(self, context):
3247 nodes, links = get_nodes_links(context)
3248 active = nodes.active
3249 output_node = None
3250 output_index = None
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
3257 for node in nodes:
3258 if node.type in output_types:
3259 output_node = node
3260 break
3261 if not output_node:
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')
3266 else:
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):
3277 output_index = i
3278 break
3279 for i, output in enumerate(active.outputs):
3280 if output.type == output_node.inputs[0].type and is_visible_socket(output):
3281 output_index = i
3282 break
3284 out_input_index = 0
3285 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3286 if active.outputs[output_index].name == 'Volume':
3287 out_input_index = 1
3288 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3289 out_input_index = 2
3290 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3292 force_update(context) # viewport render does not update
3294 return {'FINISHED'}
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)
3315 return {'FINISHED'}
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])
3336 return {'FINISHED'}
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(
3346 subtype="DIR_PATH"
3348 filename: StringProperty(
3349 subtype="FILE_NAME"
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
3360 files = self.files
3361 tree = context.space_data.node_tree
3363 # DEBUG
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"
3372 else:
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
3398 count_numbers = 0
3399 for char in reverse:
3400 if char.isdigit():
3401 count_numbers += 1
3402 else:
3403 break
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]
3412 if nodes_list:
3413 nodes_list.sort(key=lambda k: k.location.x)
3414 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3415 yloc = 0
3416 for node in nodes:
3417 node.select = False
3418 yloc += node_mid_pt(node, 'y')
3419 yloc = yloc/len(nodes)
3420 else:
3421 xloc = 0
3422 yloc = 0
3424 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3426 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3427 node = nodes.active
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
3433 node.image = img
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
3438 return {'FINISHED'}
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(
3447 subtype="DIR_PATH"
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"
3463 else:
3464 self.report({'ERROR'}, "Unsupported Node Tree type!")
3465 return {'CANCELLED'}
3467 new_nodes = []
3468 for f in self.files:
3469 fname = f.name
3471 node = nodes.new(node_type)
3472 new_nodes.append(node)
3473 node.label = fname
3474 node.hide = True
3475 node.width_hidden = 100
3476 node.location.x = xloc
3477 node.location.y = yloc
3478 yloc -= 40
3480 img = bpy.data.images.load(self.directory+fname)
3481 node.image = img
3483 # shift new nodes up to center of tree
3484 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3485 for node in nodes:
3486 if node in new_nodes:
3487 node.select = True
3488 node.location.y += (list_size/2)
3489 else:
3490 node.select = False
3491 return {'FINISHED'}
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()
3502 @classmethod
3503 def poll(cls, context):
3504 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3506 def execute(self, context):
3507 return {'FINISHED'}
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']
3517 if viewers:
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
3544 else:
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(
3556 name="Format",
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', "")),
3569 default='.png',
3572 @classmethod
3573 def poll(cls, context):
3574 valid = False
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
3579 valid = True
3580 return valid
3582 def execute(self, context):
3583 fp = self.filepath
3584 if fp:
3585 formats = {
3586 '.bmp': 'BMP',
3587 '.rgb': 'IRIS',
3588 '.png': 'PNG',
3589 '.jpg': 'JPEG',
3590 '.jpeg': 'JPEG',
3591 '.jp2': 'JPEG2000',
3592 '.tga': 'TARGA',
3593 '.cin': 'CINEON',
3594 '.dpx': 'DPX',
3595 '.exr': 'OPEN_EXR',
3596 '.hdr': 'HDR',
3597 '.tiff': 'TIFF',
3598 '.tif': 'TIFF'}
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
3607 return {'FINISHED'}
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'}
3616 @classmethod
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]
3636 success_names = []
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]
3643 if children:
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")
3654 else:
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:
3666 i.select = False
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(' ')
3677 reconnections = []
3678 mappings = chain.from_iterable([node.inputs, node.outputs])
3679 for i in (i for i in mappings if i.is_linked):
3680 for L in i.links:
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
3692 nodes.remove(node)
3693 new_node.name = props['name']
3695 if parent:
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)))
3715 return {'FINISHED'}
3719 # P A N E L
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)
3727 col.separator()
3729 col = layout.column(align=True)
3730 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3731 col.separator()
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')
3737 col.separator()
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')
3743 col.separator()
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')
3748 col.separator()
3750 col = layout.column(align=True)
3751 if mode == 'panel':
3752 row = col.row(align=True)
3753 row.operator(NWClearLabel.bl_idname).option = True
3754 row.operator(NWModifyLabels.bl_idname)
3755 else:
3756 col.operator(NWClearLabel.bl_idname).option = True
3757 col.operator(NWModifyLabels.bl_idname)
3758 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3759 col.separator()
3760 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3761 col.separator()
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')
3767 col.separator()
3769 col = layout.column(align=True)
3770 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3771 col.separator()
3773 col = layout.column(align=True)
3774 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
3775 col.separator()
3777 col = layout.column(align=True)
3778 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3779 col.separator()
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(
3790 name='prepend',
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')
3801 # M E N U S
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")
3823 props.mode = 'MIX'
3824 props.merge_type = 'ZCOMBINE'
3825 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3826 props.mode = 'MIX'
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)
3838 props.mode = 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)
3850 props.mode = type
3851 props.merge_type = 'MIX'
3854 class NWConnectionListOutputs(Menu, NWBase):
3855 bl_idname = "NODE_MT_nw_connection_list_out"
3856 bl_label = "From:"
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":
3865 index=0
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
3869 index+=1
3870 else:
3871 index=0
3872 for o in n1.outputs:
3873 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3874 index+=1
3877 class NWConnectionListInputs(Menu, NWBase):
3878 bl_idname = "NODE_MT_nw_connection_list_in"
3879 bl_label = "To:"
3881 def draw(self, context):
3882 layout = self.layout
3883 nodes, links = get_nodes_links(context)
3885 n2 = nodes[context.scene.NWLazyTarget]
3887 index = 0
3888 for i in n2.inputs:
3889 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
3890 op.from_socket = context.scene.NWSourceSocket
3891 op.to_socket = index
3892 index+=1
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)
3903 props.mode = type
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"
4037 @classmethod
4038 def poll(cls, context):
4039 valid = False
4040 if nw_check(context):
4041 snode = context.space_data
4042 valid = snode.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context)
4043 return valid
4045 def draw(self, context):
4046 l = self.layout
4047 nodes, links = get_nodes_links(context)
4048 mat = context.object.active_material
4050 objs = []
4051 for obj in bpy.data.objects:
4052 for slot in obj.material_slots:
4053 if slot.material == mat:
4054 objs.append(obj)
4055 vcols = []
4056 for obj in objs:
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
4062 if vcols:
4063 for vcol in vcols:
4064 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
4065 else:
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)
4086 else:
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"
4116 bl_label = "Input"
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"
4127 bl_label = "Output"
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"
4138 bl_label = "Shader"
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"
4160 bl_label = "Color"
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"
4171 bl_label = "Vector"
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"
4193 bl_label = "Layout"
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"
4205 bl_label = "Input"
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"
4216 bl_label = "Output"
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"
4227 bl_label = "Color"
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"
4249 bl_label = "Filter"
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"
4260 bl_label = "Vector"
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"
4271 bl_label = "Matte"
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"
4293 bl_label = "Layout"
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"
4305 bl_label = "Input"
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"
4316 bl_label = "Output"
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"
4327 bl_label = "Color"
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"
4338 bl_label = "Vector"
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"
4360 bl_label = "Layout"
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"
4372 bl_label = "Input"
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"
4383 bl_label = "Output"
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"
4394 bl_label = "Color"
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"
4449 bl_label = "Layout"
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")
4473 col.separator()
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")
4480 col.separator()
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
4516 addon_keymaps = []
4517 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4518 # props entry: (property name, property value)
4519 kmi_defs = (
4520 # MERGE NODES
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)"),
4639 # CHANGE MIX FACTOR
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
4661 # Select Children
4662 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4663 # Select Parent
4664 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4665 # Add Texture Setup
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"),
4669 # Reset backdrop
4670 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4671 # Delete unused
4672 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4673 # Frame Selected
4674 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4675 # Swap Outputs
4676 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4677 # Emission Viewer
4678 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4679 # Reload Images
4680 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4681 # Lazy Mix
4682 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4683 # Lazy Connect
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"),
4689 # Align Nodes
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"),
4693 # MENUS
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"),
4703 classes = (
4704 NWPrincipledPreferences,
4705 NWNodeWrangler,
4706 NWLazyMix,
4707 NWLazyConnect,
4708 NWDeleteUnused,
4709 NWSwapLinks,
4710 NWResetBG,
4711 NWAddAttrNode,
4712 NWEmissionViewer,
4713 NWFrameSelected,
4714 NWReloadImages,
4715 NWSwitchNodeType,
4716 NWMergeNodes,
4717 NWBatchChangeNodes,
4718 NWChangeMixFactor,
4719 NWCopySettings,
4720 NWCopyLabel,
4721 NWClearLabel,
4722 NWModifyLabels,
4723 NWAddTextureSetup,
4724 NWAddPrincipledSetup,
4725 NWAddReroutes,
4726 NWLinkActiveToSelected,
4727 NWAlignNodes,
4728 NWSelectParentChildren,
4729 NWDetachOutputs,
4730 NWLinkToOutputNode,
4731 NWMakeLink,
4732 NWCallInputsMenu,
4733 NWAddSequence,
4734 NWAddMultipleImages,
4735 NWViewerFocus,
4736 NWSaveViewer,
4737 NWResetNodes,
4738 NodeWranglerPanel,
4739 NodeWranglerMenu,
4740 NWMergeNodesMenu,
4741 NWMergeShadersMenu,
4742 NWMergeMixMenu,
4743 NWConnectionListOutputs,
4744 NWConnectionListInputs,
4745 NWMergeMathMenu,
4746 NWBatchChangeNodesMenu,
4747 NWBatchChangeBlendTypeMenu,
4748 NWBatchChangeOperationMenu,
4749 NWCopyToSelectedMenu,
4750 NWCopyLabelMenu,
4751 NWAddReroutesMenu,
4752 NWLinkActiveToSelectedMenu,
4753 NWLinkStandardMenu,
4754 NWLinkUseNodeNameMenu,
4755 NWLinkUseOutputsNamesMenu,
4756 NWVertColMenu,
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,
4791 def register():
4792 from bpy.utils import register_class
4794 # props
4795 bpy.types.Scene.NWBusyDrawing = StringProperty(
4796 name="Busy Drawing!",
4797 default="",
4798 description="An internal property used to store only the first mouse position")
4799 bpy.types.Scene.NWLazySource = StringProperty(
4800 name="Lazy Source!",
4801 default="x",
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!",
4805 default="x",
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!",
4809 default=0,
4810 description="An internal property used to store the source socket in a Lazy Connect operation")
4812 for cls in classes:
4813 register_class(cls)
4815 # keymaps
4816 addon_keymaps.clear()
4817 kc = bpy.context.window_manager.keyconfigs.addon
4818 if kc:
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)
4822 if props:
4823 for prop, value in props:
4824 setattr(kmi.properties, prop, value)
4825 addon_keymaps.append((km, kmi))
4827 # menu items
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)
4838 def unregister():
4839 from bpy.utils import unregister_class
4841 # props
4842 del bpy.types.Scene.NWBusyDrawing
4843 del bpy.types.Scene.NWLazySource
4844 del bpy.types.Scene.NWLazyTarget
4845 del bpy.types.Scene.NWSourceSocket
4847 # keymaps
4848 for km, kmi in addon_keymaps:
4849 km.keymap_items.remove(kmi)
4850 addon_keymaps.clear()
4852 # menuitems
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)
4862 for cls in classes:
4863 unregister_class(cls)
4865 if __name__ == "__main__":
4866 register()