Fix T62695: OBJ mtllib fails when filename contains spaces
[blender-addons.git] / node_wrangler.py
blobc2c267ed659310e774851365a0ea4ad8fa4e0b19
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Node Wrangler",
21 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
22 "version": (3, 35),
23 "blender": (2, 80, 0),
24 "location": "Node Editor Toolbar or Ctrl-Space",
25 "description": "Various tools to enhance and speed up node-based workflow",
26 "warning": "",
27 "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
28 "Scripts/Nodes/Nodes_Efficiency_Tools",
29 "category": "Node",
32 import bpy, blf, bgl
33 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'}
551 def nice_hotkey_name(punc):
552 # convert the ugly string name into the actual character
553 pairs = (
554 ('LEFTMOUSE', "LMB"),
555 ('MIDDLEMOUSE', "MMB"),
556 ('RIGHTMOUSE', "RMB"),
557 ('WHEELUPMOUSE', "Wheel Up"),
558 ('WHEELDOWNMOUSE', "Wheel Down"),
559 ('WHEELINMOUSE', "Wheel In"),
560 ('WHEELOUTMOUSE', "Wheel Out"),
561 ('ZERO', "0"),
562 ('ONE', "1"),
563 ('TWO', "2"),
564 ('THREE', "3"),
565 ('FOUR', "4"),
566 ('FIVE', "5"),
567 ('SIX', "6"),
568 ('SEVEN', "7"),
569 ('EIGHT', "8"),
570 ('NINE', "9"),
571 ('OSKEY', "Super"),
572 ('RET', "Enter"),
573 ('LINE_FEED', "Enter"),
574 ('SEMI_COLON', ";"),
575 ('PERIOD', "."),
576 ('COMMA', ","),
577 ('QUOTE', '"'),
578 ('MINUS', "-"),
579 ('SLASH', "/"),
580 ('BACK_SLASH', "\\"),
581 ('EQUAL', "="),
582 ('NUMPAD_1', "Numpad 1"),
583 ('NUMPAD_2', "Numpad 2"),
584 ('NUMPAD_3', "Numpad 3"),
585 ('NUMPAD_4', "Numpad 4"),
586 ('NUMPAD_5', "Numpad 5"),
587 ('NUMPAD_6', "Numpad 6"),
588 ('NUMPAD_7', "Numpad 7"),
589 ('NUMPAD_8', "Numpad 8"),
590 ('NUMPAD_9', "Numpad 9"),
591 ('NUMPAD_0', "Numpad 0"),
592 ('NUMPAD_PERIOD', "Numpad ."),
593 ('NUMPAD_SLASH', "Numpad /"),
594 ('NUMPAD_ASTERIX', "Numpad *"),
595 ('NUMPAD_MINUS', "Numpad -"),
596 ('NUMPAD_ENTER', "Numpad Enter"),
597 ('NUMPAD_PLUS', "Numpad +"),
599 nice_punc = False
600 for (ugly, nice) in pairs:
601 if punc == ugly:
602 nice_punc = nice
603 break
604 if not nice_punc:
605 nice_punc = punc.replace("_", " ").title()
606 return nice_punc
609 def force_update(context):
610 context.space_data.node_tree.update_tag()
613 def dpifac():
614 prefs = bpy.context.preferences.system
615 return prefs.dpi * prefs.pixel_size / 72
618 def node_mid_pt(node, axis):
619 if axis == 'x':
620 d = node.location.x + (node.dimensions.x / 2)
621 elif axis == 'y':
622 d = node.location.y - (node.dimensions.y / 2)
623 else:
624 d = 0
625 return d
628 def autolink(node1, node2, links):
629 link_made = False
631 for outp in node1.outputs:
632 for inp in node2.inputs:
633 if not inp.is_linked and inp.name == outp.name:
634 link_made = True
635 links.new(outp, inp)
636 return True
638 for outp in node1.outputs:
639 for inp in node2.inputs:
640 if not inp.is_linked and inp.type == outp.type:
641 link_made = True
642 links.new(outp, inp)
643 return True
645 # force some connection even if the type doesn't match
646 for outp in node1.outputs:
647 for inp in node2.inputs:
648 if not inp.is_linked:
649 link_made = True
650 links.new(outp, inp)
651 return True
653 # even if no sockets are open, force one of matching type
654 for outp in node1.outputs:
655 for inp in node2.inputs:
656 if inp.type == outp.type:
657 link_made = True
658 links.new(outp, inp)
659 return True
661 # do something!
662 for outp in node1.outputs:
663 for inp in node2.inputs:
664 link_made = True
665 links.new(outp, inp)
666 return True
668 print("Could not make a link from " + node1.name + " to " + node2.name)
669 return link_made
672 def node_at_pos(nodes, context, event):
673 nodes_near_mouse = []
674 nodes_under_mouse = []
675 target_node = None
677 store_mouse_cursor(context, event)
678 x, y = context.space_data.cursor_location
679 x = x
680 y = y
682 # Make a list of each corner (and middle of border) for each node.
683 # Will be sorted to find nearest point and thus nearest node
684 node_points_with_dist = []
685 for node in nodes:
686 skipnode = False
687 if node.type != 'FRAME': # no point trying to link to a frame node
688 locx = node.location.x
689 locy = node.location.y
690 dimx = node.dimensions.x/dpifac()
691 dimy = node.dimensions.y/dpifac()
692 if node.parent:
693 locx += node.parent.location.x
694 locy += node.parent.location.y
695 if node.parent.parent:
696 locx += node.parent.parent.location.x
697 locy += node.parent.parent.location.y
698 if node.parent.parent.parent:
699 locx += node.parent.parent.parent.location.x
700 locy += node.parent.parent.parent.location.y
701 if node.parent.parent.parent.parent:
702 # Support three levels or parenting
703 # There's got to be a better way to do this...
704 skipnode = True
705 if not skipnode:
706 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
707 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
708 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
709 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
711 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
712 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
713 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
714 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
716 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
718 for node in nodes:
719 if node.type != 'FRAME' and skipnode == False:
720 locx = node.location.x
721 locy = node.location.y
722 dimx = node.dimensions.x/dpifac()
723 dimy = node.dimensions.y/dpifac()
724 if node.parent:
725 locx += node.parent.location.x
726 locy += node.parent.location.y
727 if (locx <= x <= locx + dimx) and \
728 (locy - dimy <= y <= locy):
729 nodes_under_mouse.append(node)
731 if len(nodes_under_mouse) == 1:
732 if nodes_under_mouse[0] != nearest_node:
733 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
734 else:
735 target_node = nearest_node # else use the nearest node
736 else:
737 target_node = nearest_node
738 return target_node
741 def store_mouse_cursor(context, event):
742 space = context.space_data
743 v2d = context.region.view2d
744 tree = space.edit_tree
746 # convert mouse position to the View2D for later node placement
747 if context.region.type == 'WINDOW':
748 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
749 else:
750 space.cursor_location = tree.view_center
752 def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
753 shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
755 vertices = ((x1, y1), (x2, y2))
756 vertex_colors = ((colour[0]+(1.0-colour[0])/4,
757 colour[1]+(1.0-colour[1])/4,
758 colour[2]+(1.0-colour[2])/4,
759 colour[3]+(1.0-colour[3])/4),
760 colour)
762 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
763 bgl.glLineWidth(size * dpifac())
765 shader.bind()
766 batch.draw(shader)
769 def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
770 radius = radius * dpifac()
771 sides = 12
772 vertices = [(radius * cos(i * 2 * pi / sides) + mx,
773 radius * sin(i * 2 * pi / sides) + my)
774 for i in range(sides + 1)]
776 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
777 shader.bind()
778 shader.uniform_float("color", colour)
779 batch.draw(shader)
781 def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
782 area_width = bpy.context.area.width - (16*dpifac()) - 1
783 bottom_bar = (16*dpifac()) + 1
784 sides = 16
785 radius = radius*dpifac()
787 nlocx = (node.location.x+1)*dpifac()
788 nlocy = (node.location.y+1)*dpifac()
789 ndimx = node.dimensions.x
790 ndimy = node.dimensions.y
791 # This is a stupid way to do this... TODO use while loop
792 if node.parent:
793 nlocx += node.parent.location.x
794 nlocy += node.parent.location.y
795 if node.parent.parent:
796 nlocx += node.parent.parent.location.x
797 nlocy += node.parent.parent.location.y
798 if node.parent.parent.parent:
799 nlocx += node.parent.parent.parent.location.x
800 nlocy += node.parent.parent.parent.location.y
802 if node.hide:
803 nlocx += -1
804 nlocy += 5
805 if node.type == 'REROUTE':
806 #nlocx += 1
807 nlocy -= 1
808 ndimx = 0
809 ndimy = 0
810 radius += 6
812 # Top left corner
813 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
814 vertices = [(mx,my)]
815 for i in range(sides+1):
816 if (4<=i<=8):
817 if my > bottom_bar and mx < area_width:
818 cosine = radius * cos(i * 2 * pi / sides) + mx
819 sine = radius * sin(i * 2 * pi / sides) + my
820 vertices.append((cosine,sine))
821 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
822 shader.bind()
823 shader.uniform_float("color", colour)
824 batch.draw(shader)
826 # Top right corner
827 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
828 vertices = [(mx,my)]
829 for i in range(sides+1):
830 if (0<=i<=4):
831 if my > bottom_bar and mx < area_width:
832 cosine = radius * cos(i * 2 * pi / sides) + mx
833 sine = radius * sin(i * 2 * pi / sides) + my
834 vertices.append((cosine,sine))
835 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
836 shader.bind()
837 shader.uniform_float("color", colour)
838 batch.draw(shader)
840 # Bottom left corner
841 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
842 vertices = [(mx,my)]
843 for i in range(sides+1):
844 if (8<=i<=12):
845 if my > bottom_bar and mx < area_width:
846 cosine = radius * cos(i * 2 * pi / sides) + mx
847 sine = radius * sin(i * 2 * pi / sides) + my
848 vertices.append((cosine,sine))
849 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
850 shader.bind()
851 shader.uniform_float("color", colour)
852 batch.draw(shader)
854 # Bottom right corner
855 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
856 vertices = [(mx,my)]
857 for i in range(sides+1):
858 if (12<=i<=16):
859 if my > bottom_bar and mx < area_width:
860 cosine = radius * cos(i * 2 * pi / sides) + mx
861 sine = radius * sin(i * 2 * pi / sides) + my
862 vertices.append((cosine,sine))
863 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
864 shader.bind()
865 shader.uniform_float("color", colour)
866 batch.draw(shader)
868 # prepare drawing all edges in one batch
869 vertices = []
870 indices = []
871 id_last = 0
873 # Left edge
874 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
875 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
876 if m1x < area_width and m2x < area_width:
877 vertices.extend([(m2x-radius,m2y), (m2x,m2y),
878 (m1x,m1y), (m1x-radius,m1y)])
879 indices.extend([(id_last, id_last+1, id_last+3),
880 (id_last+3, id_last+1, id_last+2)])
881 id_last += 4
883 # Top edge
884 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
885 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
886 m1x = min(m1x, area_width)
887 m2x = min(m2x, area_width)
888 if m1y > bottom_bar and m2y > bottom_bar:
889 vertices.extend([(m1x,m1y), (m2x,m1y),
890 (m2x,m1y+radius), (m1x,m1y+radius)])
891 indices.extend([(id_last, id_last+1, id_last+3),
892 (id_last+3, id_last+1, id_last+2)])
893 id_last += 4
895 # Right edge
896 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
897 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
898 m1y = max(m1y, bottom_bar)
899 m2y = max(m2y, bottom_bar)
900 if m1x < area_width and m2x < area_width:
901 vertices.extend([(m1x,m2y), (m1x+radius,m2y),
902 (m1x+radius,m1y), (m1x,m1y)])
903 indices.extend([(id_last, id_last+1, id_last+3),
904 (id_last+3, id_last+1, id_last+2)])
905 id_last += 4
907 # Bottom edge
908 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
909 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
910 m1x = min(m1x, area_width)
911 m2x = min(m2x, area_width)
912 if m1y > bottom_bar and m2y > bottom_bar:
913 vertices.extend([(m1x,m2y), (m2x,m2y),
914 (m2x,m1y-radius), (m1x,m1y-radius)])
915 indices.extend([(id_last, id_last+1, id_last+3),
916 (id_last+3, id_last+1, id_last+2)])
918 # now draw all edges in one batch
919 if len(vertices) != 0:
920 batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
921 shader.bind()
922 shader.uniform_float("color", colour)
923 batch.draw(shader)
925 def draw_callback_nodeoutline(self, context, mode):
926 if self.mouse_path:
928 bgl.glLineWidth(1)
929 bgl.glEnable(bgl.GL_BLEND)
930 bgl.glEnable(bgl.GL_LINE_SMOOTH)
931 bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
933 nodes, links = get_nodes_links(context)
935 shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
937 if mode == "LINK":
938 col_outer = (1.0, 0.2, 0.2, 0.4)
939 col_inner = (0.0, 0.0, 0.0, 0.5)
940 col_circle_inner = (0.3, 0.05, 0.05, 1.0)
941 elif mode == "LINKMENU":
942 col_outer = (0.4, 0.6, 1.0, 0.4)
943 col_inner = (0.0, 0.0, 0.0, 0.5)
944 col_circle_inner = (0.08, 0.15, .3, 1.0)
945 elif mode == "MIX":
946 col_outer = (0.2, 1.0, 0.2, 0.4)
947 col_inner = (0.0, 0.0, 0.0, 0.5)
948 col_circle_inner = (0.05, 0.3, 0.05, 1.0)
950 m1x = self.mouse_path[0][0]
951 m1y = self.mouse_path[0][1]
952 m2x = self.mouse_path[-1][0]
953 m2y = self.mouse_path[-1][1]
955 n1 = nodes[context.scene.NWLazySource]
956 n2 = nodes[context.scene.NWLazyTarget]
958 if n1 == n2:
959 col_outer = (0.4, 0.4, 0.4, 0.4)
960 col_inner = (0.0, 0.0, 0.0, 0.5)
961 col_circle_inner = (0.2, 0.2, 0.2, 1.0)
963 draw_rounded_node_border(shader, n1, radius=6, colour=col_outer) # outline
964 draw_rounded_node_border(shader, n1, radius=5, colour=col_inner) # inner
965 draw_rounded_node_border(shader, n2, radius=6, colour=col_outer) # outline
966 draw_rounded_node_border(shader, n2, radius=5, colour=col_inner) # inner
968 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
969 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
971 # circle outline
972 draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
973 draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
975 # circle inner
976 draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
977 draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
979 bgl.glDisable(bgl.GL_BLEND)
980 bgl.glDisable(bgl.GL_LINE_SMOOTH)
982 def get_nodes_links(context):
983 tree = context.space_data.node_tree
985 # Get nodes from currently edited tree.
986 # If user is editing a group, space_data.node_tree is still the base level (outside group).
987 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
988 # the same as context.active_node, the user is in a group.
989 # Check recursively until we find the real active node_tree:
990 if tree.nodes.active:
991 while tree.nodes.active != context.active_node:
992 tree = tree.nodes.active.node_tree
994 return tree.nodes, tree.links
996 # Principled prefs
997 class NWPrincipledPreferences(bpy.types.PropertyGroup):
998 base_color: StringProperty(
999 name='Base Color',
1000 default='diffuse diff albedo base col color',
1001 description='Naming Components for Base Color maps')
1002 sss_color: StringProperty(
1003 name='Subsurface Color',
1004 default='sss subsurface',
1005 description='Naming Components for Subsurface Color maps')
1006 metallic: StringProperty(
1007 name='Metallic',
1008 default='metallic metalness metal mtl',
1009 description='Naming Components for metallness maps')
1010 specular: StringProperty(
1011 name='Specular',
1012 default='specularity specular spec spc',
1013 description='Naming Components for Specular maps')
1014 normal: StringProperty(
1015 name='Normal',
1016 default='normal nor nrm nrml norm',
1017 description='Naming Components for Normal maps')
1018 bump: StringProperty(
1019 name='Bump',
1020 default='bump bmp',
1021 description='Naming Components for bump maps')
1022 rough: StringProperty(
1023 name='Roughness',
1024 default='roughness rough rgh',
1025 description='Naming Components for roughness maps')
1026 gloss: StringProperty(
1027 name='Gloss',
1028 default='gloss glossy glossyness',
1029 description='Naming Components for glossy maps')
1030 displacement: StringProperty(
1031 name='Displacement',
1032 default='displacement displace disp dsp height heightmap',
1033 description='Naming Components for displacement maps')
1035 # Addon prefs
1036 class NWNodeWrangler(bpy.types.AddonPreferences):
1037 bl_idname = __name__
1039 merge_hide: EnumProperty(
1040 name="Hide Mix nodes",
1041 items=(
1042 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1043 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1044 ("NEVER", "Never", "Never collapse the new merge nodes")
1046 default='NON_SHADER',
1047 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1048 merge_position: EnumProperty(
1049 name="Mix Node Position",
1050 items=(
1051 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1052 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1054 default='CENTER',
1055 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1057 show_hotkey_list: BoolProperty(
1058 name="Show Hotkey List",
1059 default=False,
1060 description="Expand this box into a list of all the hotkeys for functions in this addon"
1062 hotkey_list_filter: StringProperty(
1063 name=" Filter by Name",
1064 default="",
1065 description="Show only hotkeys that have this text in their name"
1067 show_principled_lists: BoolProperty(
1068 name="Show Principled naming tags",
1069 default=False,
1070 description="Expand this box into a list of all naming tags for principled texture setup"
1072 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
1074 def draw(self, context):
1075 layout = self.layout
1076 col = layout.column()
1077 col.prop(self, "merge_position")
1078 col.prop(self, "merge_hide")
1080 box = layout.box()
1081 col = box.column(align=True)
1082 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
1083 if self.show_principled_lists:
1084 tags = self.principled_tags
1086 col.prop(tags, "base_color")
1087 col.prop(tags, "sss_color")
1088 col.prop(tags, "metallic")
1089 col.prop(tags, "specular")
1090 col.prop(tags, "rough")
1091 col.prop(tags, "gloss")
1092 col.prop(tags, "normal")
1093 col.prop(tags, "bump")
1094 col.prop(tags, "displacement")
1096 box = layout.box()
1097 col = box.column(align=True)
1098 hotkey_button_name = "Show Hotkey List"
1099 if self.show_hotkey_list:
1100 hotkey_button_name = "Hide Hotkey List"
1101 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
1102 if self.show_hotkey_list:
1103 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
1104 col.separator()
1105 for hotkey in kmi_defs:
1106 if hotkey[7]:
1107 hotkey_name = hotkey[7]
1109 if self.hotkey_list_filter.lower() in hotkey_name.lower():
1110 row = col.row(align=True)
1111 row.label(text=hotkey_name)
1112 keystr = nice_hotkey_name(hotkey[1])
1113 if hotkey[4]:
1114 keystr = "Shift " + keystr
1115 if hotkey[5]:
1116 keystr = "Alt " + keystr
1117 if hotkey[3]:
1118 keystr = "Ctrl " + keystr
1119 row.label(text=keystr)
1123 def nw_check(context):
1124 space = context.space_data
1125 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree"]
1127 valid = False
1128 if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
1129 valid = True
1131 return valid
1133 class NWBase:
1134 @classmethod
1135 def poll(cls, context):
1136 return nw_check(context)
1139 # OPERATORS
1140 class NWLazyMix(Operator, NWBase):
1141 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1142 bl_idname = "node.nw_lazy_mix"
1143 bl_label = "Mix Nodes"
1144 bl_options = {'REGISTER', 'UNDO'}
1146 def modal(self, context, event):
1147 context.area.tag_redraw()
1148 nodes, links = get_nodes_links(context)
1149 cont = True
1151 start_pos = [event.mouse_region_x, event.mouse_region_y]
1153 node1 = None
1154 if not context.scene.NWBusyDrawing:
1155 node1 = node_at_pos(nodes, context, event)
1156 if node1:
1157 context.scene.NWBusyDrawing = node1.name
1158 else:
1159 if context.scene.NWBusyDrawing != 'STOP':
1160 node1 = nodes[context.scene.NWBusyDrawing]
1162 context.scene.NWLazySource = node1.name
1163 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1165 if event.type == 'MOUSEMOVE':
1166 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1168 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1169 end_pos = [event.mouse_region_x, event.mouse_region_y]
1170 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1172 node2 = None
1173 node2 = node_at_pos(nodes, context, event)
1174 if node2:
1175 context.scene.NWBusyDrawing = node2.name
1177 if node1 == node2:
1178 cont = False
1180 if cont:
1181 if node1 and node2:
1182 for node in nodes:
1183 node.select = False
1184 node1.select = True
1185 node2.select = True
1187 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
1189 context.scene.NWBusyDrawing = ""
1190 return {'FINISHED'}
1192 elif event.type == 'ESC':
1193 print('cancelled')
1194 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1195 return {'CANCELLED'}
1197 return {'RUNNING_MODAL'}
1199 def invoke(self, context, event):
1200 if context.area.type == 'NODE_EDITOR':
1201 # the arguments we pass the the callback
1202 args = (self, context, 'MIX')
1203 # Add the region OpenGL drawing callback
1204 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1205 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1207 self.mouse_path = []
1209 context.window_manager.modal_handler_add(self)
1210 return {'RUNNING_MODAL'}
1211 else:
1212 self.report({'WARNING'}, "View3D not found, cannot run operator")
1213 return {'CANCELLED'}
1216 class NWLazyConnect(Operator, NWBase):
1217 """Connect two nodes without clicking a specific socket (automatically determined"""
1218 bl_idname = "node.nw_lazy_connect"
1219 bl_label = "Lazy Connect"
1220 bl_options = {'REGISTER', 'UNDO'}
1221 with_menu: BoolProperty()
1223 def modal(self, context, event):
1224 context.area.tag_redraw()
1225 nodes, links = get_nodes_links(context)
1226 cont = True
1228 start_pos = [event.mouse_region_x, event.mouse_region_y]
1230 node1 = None
1231 if not context.scene.NWBusyDrawing:
1232 node1 = node_at_pos(nodes, context, event)
1233 if node1:
1234 context.scene.NWBusyDrawing = node1.name
1235 else:
1236 if context.scene.NWBusyDrawing != 'STOP':
1237 node1 = nodes[context.scene.NWBusyDrawing]
1239 context.scene.NWLazySource = node1.name
1240 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1242 if event.type == 'MOUSEMOVE':
1243 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1245 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1246 end_pos = [event.mouse_region_x, event.mouse_region_y]
1247 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1249 node2 = None
1250 node2 = node_at_pos(nodes, context, event)
1251 if node2:
1252 context.scene.NWBusyDrawing = node2.name
1254 if node1 == node2:
1255 cont = False
1257 link_success = False
1258 if cont:
1259 if node1 and node2:
1260 original_sel = []
1261 original_unsel = []
1262 for node in nodes:
1263 if node.select == True:
1264 node.select = False
1265 original_sel.append(node)
1266 else:
1267 original_unsel.append(node)
1268 node1.select = True
1269 node2.select = True
1271 #link_success = autolink(node1, node2, links)
1272 if self.with_menu:
1273 if len(node1.outputs) > 1 and node2.inputs:
1274 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1275 elif len(node1.outputs) == 1:
1276 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1277 else:
1278 link_success = autolink(node1, node2, links)
1280 for node in original_sel:
1281 node.select = True
1282 for node in original_unsel:
1283 node.select = False
1285 if link_success:
1286 force_update(context)
1287 context.scene.NWBusyDrawing = ""
1288 return {'FINISHED'}
1290 elif event.type == 'ESC':
1291 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1292 return {'CANCELLED'}
1294 return {'RUNNING_MODAL'}
1296 def invoke(self, context, event):
1297 if context.area.type == 'NODE_EDITOR':
1298 nodes, links = get_nodes_links(context)
1299 node = node_at_pos(nodes, context, event)
1300 if node:
1301 context.scene.NWBusyDrawing = node.name
1303 # the arguments we pass the the callback
1304 mode = "LINK"
1305 if self.with_menu:
1306 mode = "LINKMENU"
1307 args = (self, context, mode)
1308 # Add the region OpenGL drawing callback
1309 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1310 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1312 self.mouse_path = []
1314 context.window_manager.modal_handler_add(self)
1315 return {'RUNNING_MODAL'}
1316 else:
1317 self.report({'WARNING'}, "View3D not found, cannot run operator")
1318 return {'CANCELLED'}
1321 class NWDeleteUnused(Operator, NWBase):
1322 """Delete all nodes whose output is not used"""
1323 bl_idname = 'node.nw_del_unused'
1324 bl_label = 'Delete Unused Nodes'
1325 bl_options = {'REGISTER', 'UNDO'}
1327 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1328 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1330 def is_unused_node(self, node):
1331 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1332 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1333 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1334 if node.type in end_types:
1335 return False
1337 for output in node.outputs:
1338 if output.links:
1339 return False
1340 return True
1342 @classmethod
1343 def poll(cls, context):
1344 valid = False
1345 if nw_check(context):
1346 if context.space_data.node_tree.nodes:
1347 valid = True
1348 return valid
1350 def execute(self, context):
1351 nodes, links = get_nodes_links(context)
1353 # Store selection
1354 selection = []
1355 for node in nodes:
1356 if node.select == True:
1357 selection.append(node.name)
1359 for node in nodes:
1360 node.select = False
1362 deleted_nodes = []
1363 temp_deleted_nodes = []
1364 del_unused_iterations = len(nodes)
1365 for it in range(0, del_unused_iterations):
1366 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1367 for node in nodes:
1368 if self.is_unused_node(node):
1369 node.select = True
1370 deleted_nodes.append(node.name)
1371 bpy.ops.node.delete()
1373 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1374 break
1376 if self.delete_frames:
1377 repeat = True
1378 while repeat:
1379 frames_in_use = []
1380 frames = []
1381 repeat = False
1382 for node in nodes:
1383 if node.parent:
1384 frames_in_use.append(node.parent)
1385 for node in nodes:
1386 if node.type == 'FRAME' and node not in frames_in_use:
1387 frames.append(node)
1388 if node.parent:
1389 repeat = True # repeat for nested frames
1390 for node in frames:
1391 if node not in frames_in_use:
1392 node.select = True
1393 deleted_nodes.append(node.name)
1394 bpy.ops.node.delete()
1396 if self.delete_muted:
1397 for node in nodes:
1398 if node.mute:
1399 node.select = True
1400 deleted_nodes.append(node.name)
1401 bpy.ops.node.delete_reconnect()
1403 # get unique list of deleted nodes (iterations would count the same node more than once)
1404 deleted_nodes = list(set(deleted_nodes))
1405 for n in deleted_nodes:
1406 self.report({'INFO'}, "Node " + n + " deleted")
1407 num_deleted = len(deleted_nodes)
1408 n = ' node'
1409 if num_deleted > 1:
1410 n += 's'
1411 if num_deleted:
1412 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1413 else:
1414 self.report({'INFO'}, "Nothing deleted")
1416 # Restore selection
1417 nodes, links = get_nodes_links(context)
1418 for node in nodes:
1419 if node.name in selection:
1420 node.select = True
1421 return {'FINISHED'}
1423 def invoke(self, context, event):
1424 return context.window_manager.invoke_confirm(self, event)
1427 class NWSwapLinks(Operator, NWBase):
1428 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1429 bl_idname = 'node.nw_swap_links'
1430 bl_label = 'Swap Links'
1431 bl_options = {'REGISTER', 'UNDO'}
1433 @classmethod
1434 def poll(cls, context):
1435 valid = False
1436 if nw_check(context):
1437 if context.selected_nodes:
1438 valid = len(context.selected_nodes) <= 2
1439 return valid
1441 def execute(self, context):
1442 nodes, links = get_nodes_links(context)
1443 selected_nodes = context.selected_nodes
1444 n1 = selected_nodes[0]
1446 # Swap outputs
1447 if len(selected_nodes) == 2:
1448 n2 = selected_nodes[1]
1449 if n1.outputs and n2.outputs:
1450 n1_outputs = []
1451 n2_outputs = []
1453 out_index = 0
1454 for output in n1.outputs:
1455 if output.links:
1456 for link in output.links:
1457 n1_outputs.append([out_index, link.to_socket])
1458 links.remove(link)
1459 out_index += 1
1461 out_index = 0
1462 for output in n2.outputs:
1463 if output.links:
1464 for link in output.links:
1465 n2_outputs.append([out_index, link.to_socket])
1466 links.remove(link)
1467 out_index += 1
1469 for connection in n1_outputs:
1470 try:
1471 links.new(n2.outputs[connection[0]], connection[1])
1472 except:
1473 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1474 for connection in n2_outputs:
1475 try:
1476 links.new(n1.outputs[connection[0]], connection[1])
1477 except:
1478 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1479 else:
1480 if n1.outputs or n2.outputs:
1481 self.report({'WARNING'}, "One of the nodes has no outputs!")
1482 else:
1483 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1485 # Swap Inputs
1486 elif len(selected_nodes) == 1:
1487 if n1.inputs:
1488 types = []
1490 for i1 in n1.inputs:
1491 if i1.is_linked:
1492 similar_types = 0
1493 for i2 in n1.inputs:
1494 if i1.type == i2.type and i2.is_linked:
1495 similar_types += 1
1496 types.append ([i1, similar_types, i])
1497 i += 1
1498 types.sort(key=lambda k: k[1], reverse=True)
1500 if types:
1501 t = types[0]
1502 if t[1] == 2:
1503 for i2 in n1.inputs:
1504 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1505 pair = [t[0], i2]
1506 i1f = pair[0].links[0].from_socket
1507 i1t = pair[0].links[0].to_socket
1508 i2f = pair[1].links[0].from_socket
1509 i2t = pair[1].links[0].to_socket
1510 links.new(i1f, i2t)
1511 links.new(i2f, i1t)
1512 if t[1] == 1:
1513 if len(types) == 1:
1514 fs = t[0].links[0].from_socket
1515 i = t[2]
1516 links.remove(t[0].links[0])
1517 if i+1 == len(n1.inputs):
1518 i = -1
1519 i += 1
1520 while n1.inputs[i].is_linked:
1521 i += 1
1522 links.new(fs, n1.inputs[i])
1523 elif len(types) == 2:
1524 i1f = types[0][0].links[0].from_socket
1525 i1t = types[0][0].links[0].to_socket
1526 i2f = types[1][0].links[0].from_socket
1527 i2t = types[1][0].links[0].to_socket
1528 links.new(i1f, i2t)
1529 links.new(i2f, i1t)
1531 else:
1532 self.report({'WARNING'}, "This node has no input connections to swap!")
1533 else:
1534 self.report({'WARNING'}, "This node has no inputs to swap!")
1536 force_update(context)
1537 return {'FINISHED'}
1540 class NWResetBG(Operator, NWBase):
1541 """Reset the zoom and position of the background image"""
1542 bl_idname = 'node.nw_bg_reset'
1543 bl_label = 'Reset Backdrop'
1544 bl_options = {'REGISTER', 'UNDO'}
1546 @classmethod
1547 def poll(cls, context):
1548 valid = False
1549 if nw_check(context):
1550 snode = context.space_data
1551 valid = snode.tree_type == 'CompositorNodeTree'
1552 return valid
1554 def execute(self, context):
1555 context.space_data.backdrop_zoom = 1
1556 context.space_data.backdrop_offset[0] = 0
1557 context.space_data.backdrop_offset[1] = 0
1558 return {'FINISHED'}
1561 class NWAddAttrNode(Operator, NWBase):
1562 """Add an Attribute node with this name"""
1563 bl_idname = 'node.nw_add_attr_node'
1564 bl_label = 'Add UV map'
1565 bl_options = {'REGISTER', 'UNDO'}
1567 attr_name: StringProperty()
1569 def execute(self, context):
1570 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1571 nodes, links = get_nodes_links(context)
1572 nodes.active.attribute_name = self.attr_name
1573 return {'FINISHED'}
1576 class NWEmissionViewer(Operator, NWBase):
1577 bl_idname = "node.nw_emission_viewer"
1578 bl_label = "Emission Viewer"
1579 bl_description = "Connect active node to Emission Shader for shadeless previews"
1580 bl_options = {'REGISTER', 'UNDO'}
1582 @classmethod
1583 def poll(cls, context):
1584 is_cycles = is_cycles_or_eevee(context)
1585 if nw_check(context):
1586 space = context.space_data
1587 if space.tree_type == 'ShaderNodeTree' and is_cycles:
1588 if context.active_node:
1589 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1590 return True
1591 else:
1592 return True
1593 return False
1595 def invoke(self, context, event):
1596 space = context.space_data
1597 shader_type = space.shader_type
1598 if shader_type == 'OBJECT':
1599 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1600 shader_output_type = "OUTPUT_MATERIAL"
1601 shader_output_ident = "ShaderNodeOutputMaterial"
1602 shader_viewer_ident = "ShaderNodeEmission"
1603 else:
1604 shader_output_type = "OUTPUT_LIGHT"
1605 shader_output_ident = "ShaderNodeOutputLight"
1606 shader_viewer_ident = "ShaderNodeEmission"
1608 elif shader_type == 'WORLD':
1609 shader_output_type = "OUTPUT_WORLD"
1610 shader_output_ident = "ShaderNodeOutputWorld"
1611 shader_viewer_ident = "ShaderNodeBackground"
1612 shader_types = [x[1] for x in shaders_shader_nodes_props]
1613 mlocx = event.mouse_region_x
1614 mlocy = event.mouse_region_y
1615 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1616 if 'FINISHED' in select_node: # only run if mouse click is on a node
1617 nodes, links = get_nodes_links(context)
1618 in_group = context.active_node != space.node_tree.nodes.active
1619 active = nodes.active
1620 output_types = [x[1] for x in shaders_output_nodes_props]
1621 valid = False
1622 if active:
1623 if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
1624 for out in active.outputs:
1625 if not out.hide:
1626 valid = True
1627 break
1628 if valid:
1629 # get material_output node, store selection, deselect all
1630 materialout = None # placeholder node
1631 selection = []
1632 for node in nodes:
1633 if node.type == shader_output_type:
1634 materialout = node
1635 if node.select:
1636 selection.append(node.name)
1637 node.select = False
1638 if not materialout:
1639 # get right-most location
1640 sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
1641 max_xloc_node = sorted_by_xloc[-1]
1642 if max_xloc_node.name == 'Emission Viewer':
1643 max_xloc_node = sorted_by_xloc[-2]
1645 # get average y location
1646 sum_yloc = 0
1647 for node in nodes:
1648 sum_yloc += node.location.y
1650 new_locx = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1651 new_locy = sum_yloc / len(nodes)
1653 materialout = nodes.new(shader_output_ident)
1654 materialout.location.x = new_locx
1655 materialout.location.y = new_locy
1656 materialout.select = False
1657 # Analyze outputs, add "Emission Viewer" if needed, make links
1658 out_i = None
1659 valid_outputs = []
1660 for i, out in enumerate(active.outputs):
1661 if not out.hide:
1662 valid_outputs.append(i)
1663 if valid_outputs:
1664 out_i = valid_outputs[0] # Start index of node's outputs
1665 for i, valid_i in enumerate(valid_outputs):
1666 for out_link in active.outputs[valid_i].links:
1667 if "Emission Viewer" in out_link.to_node.name or (out_link.to_node == materialout and out_link.to_socket == materialout.inputs[0]):
1668 if i < len(valid_outputs) - 1:
1669 out_i = valid_outputs[i + 1]
1670 else:
1671 out_i = valid_outputs[0]
1672 make_links = [] # store sockets for new links
1673 if active.outputs:
1674 # If output type not 'SHADER' - "Emission Viewer" needed
1675 if active.outputs[out_i].type != 'SHADER':
1676 # get Emission Viewer node
1677 emission_exists = False
1678 emission_placeholder = nodes[0]
1679 for node in nodes:
1680 if "Emission Viewer" in node.name:
1681 emission_exists = True
1682 emission_placeholder = node
1683 if not emission_exists:
1684 emission = nodes.new(shader_viewer_ident)
1685 emission.hide = True
1686 emission.location = [materialout.location.x, (materialout.location.y + 40)]
1687 emission.label = "Viewer"
1688 emission.name = "Emission Viewer"
1689 emission.use_custom_color = True
1690 emission.color = (0.6, 0.5, 0.4)
1691 emission.select = False
1692 else:
1693 emission = emission_placeholder
1694 make_links.append((active.outputs[out_i], emission.inputs[0]))
1696 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
1697 if emission.outputs[0].links.__len__() > 0:
1698 if not emission.outputs[0].links[0].to_node == materialout:
1699 make_links.append((emission.outputs[0], materialout.inputs[0]))
1700 else:
1701 make_links.append((emission.outputs[0], materialout.inputs[0]))
1703 # Set brightness of viewer to compensate for Film and CM exposure
1704 intensity = 1/context.scene.cycles.film_exposure # Film exposure is a multiplier
1705 intensity /= pow(2, (context.scene.view_settings.exposure)) # CM exposure is measured in stops/EVs (2^x)
1706 emission.inputs[1].default_value = intensity
1708 else:
1709 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
1710 make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
1711 for node in nodes:
1712 if node.name == 'Emission Viewer':
1713 node.select = True
1714 bpy.ops.node.delete()
1715 for li_from, li_to in make_links:
1716 links.new(li_from, li_to)
1717 # Restore selection
1718 nodes.active = active
1719 for node in nodes:
1720 if node.name in selection:
1721 node.select = True
1722 force_update(context)
1723 return {'FINISHED'}
1724 else:
1725 return {'CANCELLED'}
1728 class NWFrameSelected(Operator, NWBase):
1729 bl_idname = "node.nw_frame_selected"
1730 bl_label = "Frame Selected"
1731 bl_description = "Add a frame node and parent the selected nodes to it"
1732 bl_options = {'REGISTER', 'UNDO'}
1734 label_prop: StringProperty(
1735 name='Label',
1736 description='The visual name of the frame node',
1737 default=' '
1739 color_prop: FloatVectorProperty(
1740 name="Color",
1741 description="The color of the frame node",
1742 default=(0.6, 0.6, 0.6),
1743 min=0, max=1, step=1, precision=3,
1744 subtype='COLOR_GAMMA', size=3
1747 def execute(self, context):
1748 nodes, links = get_nodes_links(context)
1749 selected = []
1750 for node in nodes:
1751 if node.select == True:
1752 selected.append(node)
1754 bpy.ops.node.add_node(type='NodeFrame')
1755 frm = nodes.active
1756 frm.label = self.label_prop
1757 frm.use_custom_color = True
1758 frm.color = self.color_prop
1760 for node in selected:
1761 node.parent = frm
1763 return {'FINISHED'}
1766 class NWReloadImages(Operator, NWBase):
1767 bl_idname = "node.nw_reload_images"
1768 bl_label = "Reload Images"
1769 bl_description = "Update all the image nodes to match their files on disk"
1771 def execute(self, context):
1772 nodes, links = get_nodes_links(context)
1773 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1774 num_reloaded = 0
1775 for node in nodes:
1776 if node.type in image_types:
1777 if node.type == "TEXTURE":
1778 if node.texture: # node has texture assigned
1779 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1780 if node.texture.image: # texture has image assigned
1781 node.texture.image.reload()
1782 num_reloaded += 1
1783 else:
1784 if node.image:
1785 node.image.reload()
1786 num_reloaded += 1
1788 if num_reloaded:
1789 self.report({'INFO'}, "Reloaded images")
1790 print("Reloaded " + str(num_reloaded) + " images")
1791 force_update(context)
1792 return {'FINISHED'}
1793 else:
1794 self.report({'WARNING'}, "No images found to reload in this node tree")
1795 return {'CANCELLED'}
1798 class NWSwitchNodeType(Operator, NWBase):
1799 """Switch type of selected nodes """
1800 bl_idname = "node.nw_swtch_node_type"
1801 bl_label = "Switch Node Type"
1802 bl_options = {'REGISTER', 'UNDO'}
1804 to_type: EnumProperty(
1805 name="Switch to type",
1806 items=list(shaders_input_nodes_props) +
1807 list(shaders_output_nodes_props) +
1808 list(shaders_shader_nodes_props) +
1809 list(shaders_texture_nodes_props) +
1810 list(shaders_color_nodes_props) +
1811 list(shaders_vector_nodes_props) +
1812 list(shaders_converter_nodes_props) +
1813 list(shaders_layout_nodes_props) +
1814 list(compo_input_nodes_props) +
1815 list(compo_output_nodes_props) +
1816 list(compo_color_nodes_props) +
1817 list(compo_converter_nodes_props) +
1818 list(compo_filter_nodes_props) +
1819 list(compo_vector_nodes_props) +
1820 list(compo_matte_nodes_props) +
1821 list(compo_distort_nodes_props) +
1822 list(compo_layout_nodes_props) +
1823 list(blender_mat_input_nodes_props) +
1824 list(blender_mat_output_nodes_props) +
1825 list(blender_mat_color_nodes_props) +
1826 list(blender_mat_vector_nodes_props) +
1827 list(blender_mat_converter_nodes_props) +
1828 list(blender_mat_layout_nodes_props) +
1829 list(texture_input_nodes_props) +
1830 list(texture_output_nodes_props) +
1831 list(texture_color_nodes_props) +
1832 list(texture_pattern_nodes_props) +
1833 list(texture_textures_nodes_props) +
1834 list(texture_converter_nodes_props) +
1835 list(texture_distort_nodes_props) +
1836 list(texture_layout_nodes_props)
1839 def execute(self, context):
1840 nodes, links = get_nodes_links(context)
1841 to_type = self.to_type
1842 # Those types of nodes will not swap.
1843 src_excludes = ('NodeFrame')
1844 # Those attributes of nodes will be copied if possible
1845 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1846 'show_options', 'show_preview', 'show_texture',
1847 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1849 selected = [n for n in nodes if n.select]
1850 reselect = []
1851 for node in [n for n in selected if
1852 n.rna_type.identifier not in src_excludes and
1853 n.rna_type.identifier != to_type]:
1854 new_node = nodes.new(to_type)
1855 for attr in attrs_to_pass:
1856 if hasattr(node, attr) and hasattr(new_node, attr):
1857 setattr(new_node, attr, getattr(node, attr))
1858 # set image datablock of dst to image of src
1859 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1860 if node.image:
1861 new_node.image = node.image
1862 # Special cases
1863 if new_node.type == 'SWITCH':
1864 new_node.hide = True
1865 # Dictionaries: src_sockets and dst_sockets:
1866 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1867 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1868 # in 'INPUTS' and 'OUTPUTS':
1869 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1870 # socket entry:
1871 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1872 src_sockets = {
1873 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1874 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1876 dst_sockets = {
1877 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1878 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1880 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1881 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1882 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1883 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1884 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1885 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1886 # enumerate in inputs, then in outputs
1887 # find name, default value and links of socket
1888 for i, socket in enumerate(in_out):
1889 the_name = socket.name
1890 dval = None
1891 # Not every socket, especially in outputs has "default_value"
1892 if hasattr(socket, 'default_value'):
1893 dval = socket.default_value
1894 socket_links = []
1895 for lnk in socket.links:
1896 socket_links.append(lnk)
1897 # check type of socket to fill proper keys.
1898 for the_type in types_order_one:
1899 if socket.type == the_type:
1900 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1901 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1902 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1903 # Check which of the types in inputs/outputs is considered to be "main".
1904 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1905 for type_check in types_order_one:
1906 if sockets[in_out_name][type_check]:
1907 sockets[in_out_name]['MAIN'] = type_check
1908 break
1910 matches = {
1911 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1912 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1915 for inout, soctype in (
1916 ('INPUTS', 'MAIN',),
1917 ('INPUTS', 'SHADER',),
1918 ('INPUTS', 'RGBA',),
1919 ('INPUTS', 'VECTOR',),
1920 ('INPUTS', 'VALUE',),
1921 ('OUTPUTS', 'MAIN',),
1922 ('OUTPUTS', 'SHADER',),
1923 ('OUTPUTS', 'RGBA',),
1924 ('OUTPUTS', 'VECTOR',),
1925 ('OUTPUTS', 'VALUE',),
1927 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1928 if soctype == 'MAIN':
1929 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1930 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1931 else:
1932 sc = src_sockets[inout][soctype]
1933 dt = dst_sockets[inout][soctype]
1934 # start with 'dt' to determine number of possibilities.
1935 for i, soc in enumerate(dt):
1936 # if src main has enough entries - match them with dst main sockets by indexes.
1937 if len(sc) > i:
1938 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1939 # add 'VALUE_NAME' criterion to inputs.
1940 if inout == 'INPUTS' and soctype == 'VALUE':
1941 for s in sc:
1942 if s[2] == soc[2]: # if names match
1943 # append src (index, dval), dst (index, dval)
1944 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1946 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1947 # This creates better links when relinking textures.
1948 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1949 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1951 # Pass default values and RELINK:
1952 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1953 # INPUTS: Base on matches in proper order.
1954 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1955 # pass dvals
1956 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1957 new_node.inputs[dst_i].default_value = src_dval
1958 # Special case: switch to math
1959 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1960 new_node.type == 'MATH' and\
1961 tp == 'MAIN':
1962 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1963 new_node.inputs[dst_i].default_value = new_dst_dval
1964 if node.type == 'MIX_RGB':
1965 if node.blend_type in [o[0] for o in operations]:
1966 new_node.operation = node.blend_type
1967 # Special case: switch from math to some types
1968 if node.type == 'MATH' and\
1969 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1970 tp == 'MAIN':
1971 for i in range(3):
1972 new_node.inputs[dst_i].default_value[i] = src_dval
1973 if new_node.type == 'MIX_RGB':
1974 if node.operation in [t[0] for t in blend_types]:
1975 new_node.blend_type = node.operation
1976 # Set Fac of MIX_RGB to 1.0
1977 new_node.inputs[0].default_value = 1.0
1978 # make link only when dst matching input is not linked already.
1979 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1980 in_src_link = node.inputs[src_i].links[0]
1981 in_dst_socket = new_node.inputs[dst_i]
1982 links.new(in_src_link.from_socket, in_dst_socket)
1983 links.remove(in_src_link)
1984 # OUTPUTS: Base on matches in proper order.
1985 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1986 for out_src_link in node.outputs[src_i].links:
1987 out_dst_socket = new_node.outputs[dst_i]
1988 links.new(out_dst_socket, out_src_link.to_socket)
1989 # relink rest inputs if possible, no criteria
1990 for src_inp in node.inputs:
1991 for dst_inp in new_node.inputs:
1992 if src_inp.links and not dst_inp.links:
1993 src_link = src_inp.links[0]
1994 links.new(src_link.from_socket, dst_inp)
1995 links.remove(src_link)
1996 # relink rest outputs if possible, base on node kind if any left.
1997 for src_o in node.outputs:
1998 for out_src_link in src_o.links:
1999 for dst_o in new_node.outputs:
2000 if src_o.type == dst_o.type:
2001 links.new(dst_o, out_src_link.to_socket)
2002 # relink rest outputs no criteria if any left. Link all from first output.
2003 for src_o in node.outputs:
2004 for out_src_link in src_o.links:
2005 if new_node.outputs:
2006 links.new(new_node.outputs[0], out_src_link.to_socket)
2007 nodes.remove(node)
2008 force_update(context)
2009 return {'FINISHED'}
2012 class NWMergeNodes(Operator, NWBase):
2013 bl_idname = "node.nw_merge_nodes"
2014 bl_label = "Merge Nodes"
2015 bl_description = "Merge Selected Nodes"
2016 bl_options = {'REGISTER', 'UNDO'}
2018 mode: EnumProperty(
2019 name="mode",
2020 description="All possible blend types and math operations",
2021 items=blend_types + [op for op in operations if op not in blend_types],
2023 merge_type: EnumProperty(
2024 name="merge type",
2025 description="Type of Merge to be used",
2026 items=(
2027 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2028 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2029 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2030 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2031 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2032 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2036 def execute(self, context):
2037 settings = context.preferences.addons[__name__].preferences
2038 merge_hide = settings.merge_hide
2039 merge_position = settings.merge_position # 'center' or 'bottom'
2041 do_hide = False
2042 do_hide_shader = False
2043 if merge_hide == 'ALWAYS':
2044 do_hide = True
2045 do_hide_shader = True
2046 elif merge_hide == 'NON_SHADER':
2047 do_hide = True
2049 tree_type = context.space_data.node_tree.type
2050 if tree_type == 'COMPOSITING':
2051 node_type = 'CompositorNode'
2052 elif tree_type == 'SHADER':
2053 node_type = 'ShaderNode'
2054 elif tree_type == 'TEXTURE':
2055 node_type = 'TextureNode'
2056 nodes, links = get_nodes_links(context)
2057 mode = self.mode
2058 merge_type = self.merge_type
2059 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2060 # 'ZCOMBINE' works only if mode == 'MIX'
2061 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2062 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2063 merge_type = 'MIX'
2064 mode = 'MIX'
2065 selected_mix = [] # entry = [index, loc]
2066 selected_shader = [] # entry = [index, loc]
2067 selected_math = [] # entry = [index, loc]
2068 selected_z = [] # entry = [index, loc]
2069 selected_alphaover = [] # entry = [index, loc]
2071 for i, node in enumerate(nodes):
2072 if node.select and node.outputs:
2073 if merge_type == 'AUTO':
2074 for (type, types_list, dst) in (
2075 ('SHADER', ('MIX', 'ADD'), selected_shader),
2076 ('RGBA', [t[0] for t in blend_types], selected_mix),
2077 ('VALUE', [t[0] for t in operations], selected_math),
2079 output_type = node.outputs[0].type
2080 valid_mode = mode in types_list
2081 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2082 # Cheat that output type is 'RGBA',
2083 # and that 'MIX' exists in math operations list.
2084 # This way when selected_mix list is analyzed:
2085 # Node data will be appended even though it doesn't meet requirements.
2086 if output_type != 'SHADER' and mode == 'MIX':
2087 output_type = 'RGBA'
2088 valid_mode = True
2089 if output_type == type and valid_mode:
2090 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2091 else:
2092 for (type, types_list, dst) in (
2093 ('SHADER', ('MIX', 'ADD'), selected_shader),
2094 ('MIX', [t[0] for t in blend_types], selected_mix),
2095 ('MATH', [t[0] for t in operations], selected_math),
2096 ('ZCOMBINE', ('MIX', ), selected_z),
2097 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2099 if merge_type == type and mode in types_list:
2100 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2101 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2102 # use only 'Mix' nodes for merging.
2103 # For that we add selected_math list to selected_mix list and clear selected_math.
2104 if selected_mix and selected_math and merge_type == 'AUTO':
2105 selected_mix += selected_math
2106 selected_math = []
2108 for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
2109 if nodes_list:
2110 count_before = len(nodes)
2111 # sort list by loc_x - reversed
2112 nodes_list.sort(key=lambda k: k[1], reverse=True)
2113 # get maximum loc_x
2114 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2115 nodes_list.sort(key=lambda k: k[2], reverse=True)
2116 if merge_position == 'CENTER':
2117 loc_y = ((nodes_list[len(nodes_list) - 1][2]) + (nodes_list[len(nodes_list) - 2][2])) / 2 # average yloc of last two nodes (lowest two)
2118 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2119 if do_hide:
2120 loc_y += 40
2121 else:
2122 loc_y += 80
2123 else:
2124 loc_y = nodes_list[len(nodes_list) - 1][2]
2125 offset_y = 100
2126 if not do_hide:
2127 offset_y = 200
2128 if nodes_list == selected_shader and not do_hide_shader:
2129 offset_y = 150.0
2130 the_range = len(nodes_list) - 1
2131 if len(nodes_list) == 1:
2132 the_range = 1
2133 for i in range(the_range):
2134 if nodes_list == selected_mix:
2135 add_type = node_type + 'MixRGB'
2136 add = nodes.new(add_type)
2137 add.blend_type = mode
2138 if mode != 'MIX':
2139 add.inputs[0].default_value = 1.0
2140 add.show_preview = False
2141 add.hide = do_hide
2142 if do_hide:
2143 loc_y = loc_y - 50
2144 first = 1
2145 second = 2
2146 add.width_hidden = 100.0
2147 elif nodes_list == selected_math:
2148 add_type = node_type + 'Math'
2149 add = nodes.new(add_type)
2150 add.operation = mode
2151 add.hide = do_hide
2152 if do_hide:
2153 loc_y = loc_y - 50
2154 first = 0
2155 second = 1
2156 add.width_hidden = 100.0
2157 elif nodes_list == selected_shader:
2158 if mode == 'MIX':
2159 add_type = node_type + 'MixShader'
2160 add = nodes.new(add_type)
2161 add.hide = do_hide_shader
2162 if do_hide_shader:
2163 loc_y = loc_y - 50
2164 first = 1
2165 second = 2
2166 add.width_hidden = 100.0
2167 elif mode == 'ADD':
2168 add_type = node_type + 'AddShader'
2169 add = nodes.new(add_type)
2170 add.hide = do_hide_shader
2171 if do_hide_shader:
2172 loc_y = loc_y - 50
2173 first = 0
2174 second = 1
2175 add.width_hidden = 100.0
2176 elif nodes_list == selected_z:
2177 add = nodes.new('CompositorNodeZcombine')
2178 add.show_preview = False
2179 add.hide = do_hide
2180 if do_hide:
2181 loc_y = loc_y - 50
2182 first = 0
2183 second = 2
2184 add.width_hidden = 100.0
2185 elif nodes_list == selected_alphaover:
2186 add = nodes.new('CompositorNodeAlphaOver')
2187 add.show_preview = False
2188 add.hide = do_hide
2189 if do_hide:
2190 loc_y = loc_y - 50
2191 first = 1
2192 second = 2
2193 add.width_hidden = 100.0
2194 add.location = loc_x, loc_y
2195 loc_y += offset_y
2196 add.select = True
2197 count_adds = i + 1
2198 count_after = len(nodes)
2199 index = count_after - 1
2200 first_selected = nodes[nodes_list[0][0]]
2201 # "last" node has been added as first, so its index is count_before.
2202 last_add = nodes[count_before]
2203 # Special case:
2204 # Two nodes were selected and first selected has no output links, second selected has output links.
2205 # Then add links from last add to all links 'to_socket' of out links of second selected.
2206 if len(nodes_list) == 2:
2207 if not first_selected.outputs[0].links:
2208 second_selected = nodes[nodes_list[1][0]]
2209 for ss_link in second_selected.outputs[0].links:
2210 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2211 # Create list of invalid indexes.
2212 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2213 # Link only if "to_node" index not in invalid indexes list.
2214 if ss_link.to_node not in [nodes[i] for i in invalid_i]:
2215 links.new(last_add.outputs[0], ss_link.to_socket)
2216 # add links from last_add to all links 'to_socket' of out links of first selected.
2217 for fs_link in first_selected.outputs[0].links:
2218 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2219 # Create list of invalid indexes.
2220 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2221 # Link only if "to_node" index not in invalid indexes list.
2222 if fs_link.to_node not in [nodes[i] for i in invalid_i]:
2223 links.new(last_add.outputs[0], fs_link.to_socket)
2224 # add link from "first" selected and "first" add node
2225 node_to = nodes[count_after - 1]
2226 links.new(first_selected.outputs[0], node_to.inputs[first])
2227 if node_to.type == 'ZCOMBINE':
2228 for fs_out in first_selected.outputs:
2229 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
2230 links.new(fs_out, node_to.inputs[1])
2231 break
2232 # add links between added ADD nodes and between selected and ADD nodes
2233 for i in range(count_adds):
2234 if i < count_adds - 1:
2235 node_from = nodes[index]
2236 node_to = nodes[index - 1]
2237 node_to_input_i = first
2238 node_to_z_i = 1 # if z combine - link z to first z input
2239 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2240 if node_to.type == 'ZCOMBINE':
2241 for from_out in node_from.outputs:
2242 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2243 links.new(from_out, node_to.inputs[node_to_z_i])
2244 if len(nodes_list) > 1:
2245 node_from = nodes[nodes_list[i + 1][0]]
2246 node_to = nodes[index]
2247 node_to_input_i = second
2248 node_to_z_i = 3 # if z combine - link z to second z input
2249 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2250 if node_to.type == 'ZCOMBINE':
2251 for from_out in node_from.outputs:
2252 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2253 links.new(from_out, node_to.inputs[node_to_z_i])
2254 index -= 1
2255 # set "last" of added nodes as active
2256 nodes.active = last_add
2257 for i, x, y, dx, h in nodes_list:
2258 nodes[i].select = False
2260 return {'FINISHED'}
2263 class NWBatchChangeNodes(Operator, NWBase):
2264 bl_idname = "node.nw_batch_change"
2265 bl_label = "Batch Change"
2266 bl_description = "Batch Change Blend Type and Math Operation"
2267 bl_options = {'REGISTER', 'UNDO'}
2269 blend_type: EnumProperty(
2270 name="Blend Type",
2271 items=blend_types + navs,
2273 operation: EnumProperty(
2274 name="Operation",
2275 items=operations + navs,
2278 def execute(self, context):
2280 nodes, links = get_nodes_links(context)
2281 blend_type = self.blend_type
2282 operation = self.operation
2283 for node in context.selected_nodes:
2284 if node.type == 'MIX_RGB':
2285 if not blend_type in [nav[0] for nav in navs]:
2286 node.blend_type = blend_type
2287 else:
2288 if blend_type == 'NEXT':
2289 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2290 #index = blend_types.index(node.blend_type)
2291 if index == len(blend_types) - 1:
2292 node.blend_type = blend_types[0][0]
2293 else:
2294 node.blend_type = blend_types[index + 1][0]
2296 if blend_type == 'PREV':
2297 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2298 if index == 0:
2299 node.blend_type = blend_types[len(blend_types) - 1][0]
2300 else:
2301 node.blend_type = blend_types[index - 1][0]
2303 if node.type == 'MATH':
2304 if not operation in [nav[0] for nav in navs]:
2305 node.operation = operation
2306 else:
2307 if operation == 'NEXT':
2308 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2309 #index = operations.index(node.operation)
2310 if index == len(operations) - 1:
2311 node.operation = operations[0][0]
2312 else:
2313 node.operation = operations[index + 1][0]
2315 if operation == 'PREV':
2316 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2317 #index = operations.index(node.operation)
2318 if index == 0:
2319 node.operation = operations[len(operations) - 1][0]
2320 else:
2321 node.operation = operations[index - 1][0]
2323 return {'FINISHED'}
2326 class NWChangeMixFactor(Operator, NWBase):
2327 bl_idname = "node.nw_factor"
2328 bl_label = "Change Factor"
2329 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2330 bl_options = {'REGISTER', 'UNDO'}
2332 # option: Change factor.
2333 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2334 # Else - change factor by option value.
2335 option: FloatProperty()
2337 def execute(self, context):
2338 nodes, links = get_nodes_links(context)
2339 option = self.option
2340 selected = [] # entry = index
2341 for si, node in enumerate(nodes):
2342 if node.select:
2343 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2344 selected.append(si)
2346 for si in selected:
2347 fac = nodes[si].inputs[0]
2348 nodes[si].hide = False
2349 if option in {0.0, 1.0}:
2350 fac.default_value = option
2351 else:
2352 fac.default_value += option
2354 return {'FINISHED'}
2357 class NWCopySettings(Operator, NWBase):
2358 bl_idname = "node.nw_copy_settings"
2359 bl_label = "Copy Settings"
2360 bl_description = "Copy Settings of Active Node to Selected Nodes"
2361 bl_options = {'REGISTER', 'UNDO'}
2363 @classmethod
2364 def poll(cls, context):
2365 valid = False
2366 if nw_check(context):
2367 if (
2368 context.active_node is not None and
2369 context.active_node.type != 'FRAME'
2371 valid = True
2372 return valid
2374 def execute(self, context):
2375 node_active = context.active_node
2376 node_selected = context.selected_nodes
2378 # Error handling
2379 if not (len(node_selected) > 1):
2380 self.report({'ERROR'}, "2 nodes must be selected at least")
2381 return {'CANCELLED'}
2383 # Check if active node is in the selection
2384 selected_node_names = [n.name for n in node_selected]
2385 if node_active.name not in selected_node_names:
2386 self.report({'ERROR'}, "No active node")
2387 return {'CANCELLED'}
2389 # Get nodes in selection by type
2390 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2392 if not (len(valid_nodes) > 1) and node_active:
2393 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2394 return {'CANCELLED'}
2396 if len(valid_nodes) != len(node_selected):
2397 # Report nodes that are not valid
2398 valid_node_names = [n.name for n in valid_nodes]
2399 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2400 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2402 # Reference original
2403 orig = node_active
2404 #node_selected_names = [n.name for n in node_selected]
2406 # Output list
2407 success_names = []
2409 # Deselect all nodes
2410 for i in node_selected:
2411 i.select = False
2413 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2414 # Run through all other nodes
2415 for node in valid_nodes[1:]:
2417 # Check for frame node
2418 parent = node.parent if node.parent else None
2419 node_loc = [node.location.x, node.location.y]
2421 # Select original to duplicate
2422 orig.select = True
2424 # Duplicate selected node
2425 bpy.ops.node.duplicate()
2426 new_node = context.selected_nodes[0]
2428 # Deselect copy
2429 new_node.select = False
2431 # Properties to copy
2432 node_tree = node.id_data
2433 props_to_copy = 'bl_idname name location height width'.split(' ')
2435 # Input and outputs
2436 reconnections = []
2437 mappings = chain.from_iterable([node.inputs, node.outputs])
2438 for i in (i for i in mappings if i.is_linked):
2439 for L in i.links:
2440 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2442 # Properties
2443 props = {j: getattr(node, j) for j in props_to_copy}
2444 props_to_copy.pop(0)
2446 for prop in props_to_copy:
2447 setattr(new_node, prop, props[prop])
2449 # Get the node tree to remove the old node
2450 nodes = node_tree.nodes
2451 nodes.remove(node)
2452 new_node.name = props['name']
2454 if parent:
2455 new_node.parent = parent
2456 new_node.location = node_loc
2458 for str_from, str_to in reconnections:
2459 node_tree.links.new(eval(str_from), eval(str_to))
2461 success_names.append(new_node.name)
2463 orig.select = True
2464 node_tree.nodes.active = orig
2465 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2466 return {'FINISHED'}
2469 class NWCopyLabel(Operator, NWBase):
2470 bl_idname = "node.nw_copy_label"
2471 bl_label = "Copy Label"
2472 bl_options = {'REGISTER', 'UNDO'}
2474 option: EnumProperty(
2475 name="option",
2476 description="Source of name of label",
2477 items=(
2478 ('FROM_ACTIVE', 'from active', 'from active node',),
2479 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2480 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2484 def execute(self, context):
2485 nodes, links = get_nodes_links(context)
2486 option = self.option
2487 active = nodes.active
2488 if option == 'FROM_ACTIVE':
2489 if active:
2490 src_label = active.label
2491 for node in [n for n in nodes if n.select and nodes.active != n]:
2492 node.label = src_label
2493 elif option == 'FROM_NODE':
2494 selected = [n for n in nodes if n.select]
2495 for node in selected:
2496 for input in node.inputs:
2497 if input.links:
2498 src = input.links[0].from_node
2499 node.label = src.label
2500 break
2501 elif option == 'FROM_SOCKET':
2502 selected = [n for n in nodes if n.select]
2503 for node in selected:
2504 for input in node.inputs:
2505 if input.links:
2506 src = input.links[0].from_socket
2507 node.label = src.name
2508 break
2510 return {'FINISHED'}
2513 class NWClearLabel(Operator, NWBase):
2514 bl_idname = "node.nw_clear_label"
2515 bl_label = "Clear Label"
2516 bl_options = {'REGISTER', 'UNDO'}
2518 option: BoolProperty()
2520 def execute(self, context):
2521 nodes, links = get_nodes_links(context)
2522 for node in [n for n in nodes if n.select]:
2523 node.label = ''
2525 return {'FINISHED'}
2527 def invoke(self, context, event):
2528 if self.option:
2529 return self.execute(context)
2530 else:
2531 return context.window_manager.invoke_confirm(self, event)
2534 class NWModifyLabels(Operator, NWBase):
2535 """Modify Labels of all selected nodes"""
2536 bl_idname = "node.nw_modify_labels"
2537 bl_label = "Modify Labels"
2538 bl_options = {'REGISTER', 'UNDO'}
2540 prepend: StringProperty(
2541 name="Add to Beginning"
2543 append: StringProperty(
2544 name="Add to End"
2546 replace_from: StringProperty(
2547 name="Text to Replace"
2549 replace_to: StringProperty(
2550 name="Replace with"
2553 def execute(self, context):
2554 nodes, links = get_nodes_links(context)
2555 for node in [n for n in nodes if n.select]:
2556 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2558 return {'FINISHED'}
2560 def invoke(self, context, event):
2561 self.prepend = ""
2562 self.append = ""
2563 self.remove = ""
2564 return context.window_manager.invoke_props_dialog(self)
2567 class NWAddTextureSetup(Operator, NWBase):
2568 bl_idname = "node.nw_add_texture"
2569 bl_label = "Texture Setup"
2570 bl_description = "Add Texture Node Setup to Selected Shaders"
2571 bl_options = {'REGISTER', 'UNDO'}
2573 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2575 @classmethod
2576 def poll(cls, context):
2577 valid = False
2578 if nw_check(context):
2579 space = context.space_data
2580 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2581 valid = True
2582 return valid
2584 def execute(self, context):
2585 nodes, links = get_nodes_links(context)
2586 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2587 texture_types = [x[1] for x in shaders_texture_nodes_props]
2588 selected_nodes = [n for n in nodes if n.select]
2589 for t_node in selected_nodes:
2590 valid = False
2591 input_index = 0
2592 if t_node.inputs:
2593 for index, i in enumerate(t_node.inputs):
2594 if not i.is_linked:
2595 valid = True
2596 input_index = index
2597 break
2598 if valid:
2599 locx = t_node.location.x
2600 locy = t_node.location.y - t_node.dimensions.y/2
2602 xoffset = [500, 700]
2603 is_texture = False
2604 if t_node.type in texture_types + ['MAPPING']:
2605 xoffset = [290, 500]
2606 is_texture = True
2608 coordout = 2
2609 image_type = 'ShaderNodeTexImage'
2611 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
2612 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2613 if t_node.type == 'BACKGROUND':
2614 image_type = 'ShaderNodeTexEnvironment'
2616 if not is_texture:
2617 tex = nodes.new(image_type)
2618 tex.location = [locx - 200, locy + 112]
2619 nodes.active = tex
2620 links.new(tex.outputs[0], t_node.inputs[input_index])
2622 t_node.select = False
2623 if self.add_mapping or is_texture:
2624 if t_node.type != 'MAPPING':
2625 m = nodes.new('ShaderNodeMapping')
2626 m.location = [locx - xoffset[0], locy + 141]
2627 m.width = 240
2628 else:
2629 m = t_node
2630 coord = nodes.new('ShaderNodeTexCoord')
2631 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
2633 if not is_texture:
2634 links.new(m.outputs[0], tex.inputs[0])
2635 links.new(coord.outputs[coordout], m.inputs[0])
2636 else:
2637 nodes.active = m
2638 links.new(m.outputs[0], t_node.inputs[input_index])
2639 links.new(coord.outputs[coordout], m.inputs[0])
2640 else:
2641 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
2642 return {'FINISHED'}
2645 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2646 bl_idname = "node.nw_add_textures_for_principled"
2647 bl_label = "Principled Texture Setup"
2648 bl_description = "Add Texture Node Setup for Principled BSDF"
2649 bl_options = {'REGISTER', 'UNDO'}
2651 directory: StringProperty(
2652 name='Directory',
2653 subtype='DIR_PATH',
2654 default='',
2655 description='Folder to search in for image files'
2657 files: CollectionProperty(
2658 type=bpy.types.OperatorFileListElement,
2659 options={'HIDDEN', 'SKIP_SAVE'}
2662 order = [
2663 "filepath",
2664 "files",
2667 @classmethod
2668 def poll(cls, context):
2669 valid = False
2670 if nw_check(context):
2671 space = context.space_data
2672 if space.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
2673 valid = True
2674 return valid
2676 def execute(self, context):
2677 # Check if everything is ok
2678 if not self.directory:
2679 self.report({'INFO'}, 'No Folder Selected')
2680 return {'CANCELLED'}
2681 if not self.files[:]:
2682 self.report({'INFO'}, 'No Files Selected')
2683 return {'CANCELLED'}
2685 nodes, links = get_nodes_links(context)
2686 active_node = nodes.active
2687 if not active_node.bl_idname == 'ShaderNodeBsdfPrincipled':
2688 self.report({'INFO'}, 'Select Principled BSDF')
2689 return {'CANCELLED'}
2691 # Helper_functions
2692 def split_into__components(fname):
2693 # Split filename into components
2694 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2695 # Remove extension
2696 fname = path.splitext(fname)[0]
2697 # Remove digits
2698 fname = ''.join(i for i in fname if not i.isdigit())
2699 # Separate CamelCase by space
2700 fname = re.sub("([a-z])([A-Z])","\g<1> \g<2>",fname)
2701 # Replace common separators with SPACE
2702 seperators = ['_', '.', '-', '__', '--', '#']
2703 for sep in seperators:
2704 fname = fname.replace(sep, ' ')
2706 components = fname.split(' ')
2707 components = [c.lower() for c in components]
2708 return components
2710 # Filter textures names for texturetypes in filenames
2711 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2712 tags = context.preferences.addons[__name__].preferences.principled_tags
2713 normal_abbr = tags.normal.split(' ')
2714 bump_abbr = tags.bump.split(' ')
2715 gloss_abbr = tags.gloss.split(' ')
2716 rough_abbr = tags.rough.split(' ')
2717 socketnames = [
2718 ['Displacement', tags.displacement.split(' '), None],
2719 ['Base Color', tags.base_color.split(' '), None],
2720 ['Subsurface Color', tags.sss_color.split(' '), None],
2721 ['Metallic', tags.metallic.split(' '), None],
2722 ['Specular', tags.specular.split(' '), None],
2723 ['Roughness', rough_abbr + gloss_abbr, None],
2724 ['Normal', normal_abbr + bump_abbr, None],
2727 # Look through texture_types and set value as filename of first matched file
2728 def match_files_to_socket_names():
2729 for sname in socketnames:
2730 for file in self.files:
2731 fname = file.name
2732 filenamecomponents = split_into__components(fname)
2733 matches = set(sname[1]).intersection(set(filenamecomponents))
2734 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2735 if matches:
2736 sname[2] = fname
2737 break
2739 match_files_to_socket_names()
2740 # Remove socketnames without found files
2741 socketnames = [s for s in socketnames if s[2]
2742 and path.exists(self.directory+s[2])]
2743 if not socketnames:
2744 self.report({'INFO'}, 'No matching images found')
2745 print('No matching images found')
2746 return {'CANCELLED'}
2748 # Add found images
2749 print('\nMatched Textures:')
2750 texture_nodes = []
2751 disp_texture = None
2752 normal_node = None
2753 roughness_node = None
2754 for i, sname in enumerate(socketnames):
2755 print(i, sname[0], sname[2])
2757 # DISPLACEMENT NODES
2758 if sname[0] == 'Displacement':
2759 disp_texture = nodes.new(type='ShaderNodeTexImage')
2760 img = bpy.data.images.load(self.directory+sname[2])
2761 disp_texture.image = img
2762 disp_texture.label = 'Displacement'
2763 disp_texture.color_space = 'NONE'
2765 # Add displacement offset nodes
2766 disp_node = nodes.new(type='ShaderNodeDisplacement')
2767 disp_node.location = active_node.location + Vector((0, -560))
2768 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
2770 # TODO Turn on true displacement in the material
2771 # Too complicated for now
2773 # Find output node
2774 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2775 if output_node:
2776 if not output_node[0].inputs[2].is_linked:
2777 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
2779 continue
2781 if not active_node.inputs[sname[0]].is_linked:
2782 # No texture node connected -> add texture node with new image
2783 texture_node = nodes.new(type='ShaderNodeTexImage')
2784 img = bpy.data.images.load(self.directory+sname[2])
2785 texture_node.image = img
2787 # NORMAL NODES
2788 if sname[0] == 'Normal':
2789 # Test if new texture node is normal or bump map
2790 fname_components = split_into__components(sname[2])
2791 match_normal = set(normal_abbr).intersection(set(fname_components))
2792 match_bump = set(bump_abbr).intersection(set(fname_components))
2793 if match_normal:
2794 # If Normal add normal node in between
2795 normal_node = nodes.new(type='ShaderNodeNormalMap')
2796 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
2797 elif match_bump:
2798 # If Bump add bump node in between
2799 normal_node = nodes.new(type='ShaderNodeBump')
2800 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
2802 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
2803 normal_node_texture = texture_node
2805 elif sname[0] == 'Roughness':
2806 # Test if glossy or roughness map
2807 fname_components = split_into__components(sname[2])
2808 match_rough = set(rough_abbr).intersection(set(fname_components))
2809 match_gloss = set(gloss_abbr).intersection(set(fname_components))
2811 if match_rough:
2812 # If Roughness nothing to to
2813 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2815 elif match_gloss:
2816 # If Gloss Map add invert node
2817 invert_node = nodes.new(type='ShaderNodeInvert')
2818 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
2820 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
2821 roughness_node = texture_node
2823 else:
2824 # This is a simple connection Texture --> Input slot
2825 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2827 # Use non-color for all but 'Base Color' Textures
2828 if not sname[0] in ['Base Color']:
2829 texture_node.color_space = 'NONE'
2831 else:
2832 # If already texture connected. add to node list for alignment
2833 texture_node = active_node.inputs[sname[0]].links[0].from_node
2835 # This are all connected texture nodes
2836 texture_nodes.append(texture_node)
2837 texture_node.label = sname[0]
2839 if disp_texture:
2840 texture_nodes.append(disp_texture)
2842 # Alignment
2843 for i, texture_node in enumerate(texture_nodes):
2844 offset = Vector((-550, (i * -280) + 200))
2845 texture_node.location = active_node.location + offset
2847 if normal_node:
2848 # Extra alignment if normal node was added
2849 normal_node.location = normal_node_texture.location + Vector((300, 0))
2851 if roughness_node:
2852 # Alignment of invert node if glossy map
2853 invert_node.location = roughness_node.location + Vector((300, 0))
2855 # Add texture input + mapping
2856 mapping = nodes.new(type='ShaderNodeMapping')
2857 mapping.location = active_node.location + Vector((-1050, 0))
2858 if len(texture_nodes) > 1:
2859 # If more than one texture add reroute node in between
2860 reroute = nodes.new(type='NodeReroute')
2861 texture_nodes.append(reroute)
2862 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
2863 reroute.location = tex_coords + Vector((-50, -120))
2864 for texture_node in texture_nodes:
2865 link = links.new(texture_node.inputs[0], reroute.outputs[0])
2866 link = links.new(reroute.inputs[0], mapping.outputs[0])
2867 else:
2868 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
2870 # Connect texture_coordiantes to mapping node
2871 texture_input = nodes.new(type='ShaderNodeTexCoord')
2872 texture_input.location = mapping.location + Vector((-200, 0))
2873 link = links.new(mapping.inputs[0], texture_input.outputs[2])
2875 # Create frame around tex coords and mapping
2876 frame = nodes.new(type='NodeFrame')
2877 frame.label = 'Mapping'
2878 mapping.parent = frame
2879 texture_input.parent = frame
2880 frame.update()
2882 # Create frame around texture nodes
2883 frame = nodes.new(type='NodeFrame')
2884 frame.label = 'Textures'
2885 for tnode in texture_nodes:
2886 tnode.parent = frame
2887 frame.update()
2889 # Just to be sure
2890 active_node.select = False
2891 nodes.update()
2892 links.update()
2893 force_update(context)
2894 return {'FINISHED'}
2897 class NWAddReroutes(Operator, NWBase):
2898 """Add Reroute Nodes and link them to outputs of selected nodes"""
2899 bl_idname = "node.nw_add_reroutes"
2900 bl_label = "Add Reroutes"
2901 bl_description = "Add Reroutes to Outputs"
2902 bl_options = {'REGISTER', 'UNDO'}
2904 option: EnumProperty(
2905 name="option",
2906 items=[
2907 ('ALL', 'to all', 'Add to all outputs'),
2908 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2909 ('LINKED', 'to linked', 'Add only to linked outputs'),
2913 def execute(self, context):
2914 tree_type = context.space_data.node_tree.type
2915 option = self.option
2916 nodes, links = get_nodes_links(context)
2917 # output valid when option is 'all' or when 'loose' output has no links
2918 valid = False
2919 post_select = [] # nodes to be selected after execution
2920 # create reroutes and recreate links
2921 for node in [n for n in nodes if n.select]:
2922 if node.outputs:
2923 x = node.location.x
2924 y = node.location.y
2925 width = node.width
2926 # unhide 'REROUTE' nodes to avoid issues with location.y
2927 if node.type == 'REROUTE':
2928 node.hide = False
2929 # When node is hidden - width_hidden not usable.
2930 # Hack needed to calculate real width
2931 if node.hide:
2932 bpy.ops.node.select_all(action='DESELECT')
2933 helper = nodes.new('NodeReroute')
2934 helper.select = True
2935 node.select = True
2936 # resize node and helper to zero. Then check locations to calculate width
2937 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2938 width = 2.0 * (helper.location.x - node.location.x)
2939 # restore node location
2940 node.location = x, y
2941 # delete helper
2942 node.select = False
2943 # only helper is selected now
2944 bpy.ops.node.delete()
2945 x = node.location.x + width + 20.0
2946 if node.type != 'REROUTE':
2947 y -= 35.0
2948 y_offset = -22.0
2949 loc = x, y
2950 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
2951 for out_i, output in enumerate(node.outputs):
2952 pass_used = False # initial value to be analyzed if 'R_LAYERS'
2953 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
2954 if node.type != 'R_LAYERS':
2955 pass_used = True
2956 else: # if 'R_LAYERS' check if output represent used render pass
2957 node_scene = node.scene
2958 node_layer = node.layer
2959 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
2960 if output.name == 'Alpha':
2961 pass_used = True
2962 else:
2963 # check entries in global 'rl_outputs' variable
2964 for rlo in rl_outputs:
2965 if output.name in {rlo.output_name, rlo.exr_output_name}:
2966 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
2967 break
2968 if pass_used:
2969 valid = ((option == 'ALL') or
2970 (option == 'LOOSE' and not output.links) or
2971 (option == 'LINKED' and output.links))
2972 # Add reroutes only if valid, but offset location in all cases.
2973 if valid:
2974 n = nodes.new('NodeReroute')
2975 nodes.active = n
2976 for link in output.links:
2977 links.new(n.outputs[0], link.to_socket)
2978 links.new(output, n.inputs[0])
2979 n.location = loc
2980 post_select.append(n)
2981 reroutes_count += 1
2982 y += y_offset
2983 loc = x, y
2984 # disselect the node so that after execution of script only newly created nodes are selected
2985 node.select = False
2986 # nicer reroutes distribution along y when node.hide
2987 if node.hide:
2988 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
2989 for reroute in [r for r in nodes if r.select]:
2990 reroute.location.y -= y_translate
2991 for node in post_select:
2992 node.select = True
2994 return {'FINISHED'}
2997 class NWLinkActiveToSelected(Operator, NWBase):
2998 """Link active node to selected nodes basing on various criteria"""
2999 bl_idname = "node.nw_link_active_to_selected"
3000 bl_label = "Link Active Node to Selected"
3001 bl_options = {'REGISTER', 'UNDO'}
3003 replace: BoolProperty()
3004 use_node_name: BoolProperty()
3005 use_outputs_names: BoolProperty()
3007 @classmethod
3008 def poll(cls, context):
3009 valid = False
3010 if nw_check(context):
3011 if context.active_node is not None:
3012 if context.active_node.select:
3013 valid = True
3014 return valid
3016 def execute(self, context):
3017 nodes, links = get_nodes_links(context)
3018 replace = self.replace
3019 use_node_name = self.use_node_name
3020 use_outputs_names = self.use_outputs_names
3021 active = nodes.active
3022 selected = [node for node in nodes if node.select and node != active]
3023 outputs = [] # Only usable outputs of active nodes will be stored here.
3024 for out in active.outputs:
3025 if active.type != 'R_LAYERS':
3026 outputs.append(out)
3027 else:
3028 # 'R_LAYERS' node type needs special handling.
3029 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3030 # Only outputs that represent used passes should be taken into account
3031 # Check if pass represented by output is used.
3032 # global 'rl_outputs' list will be used for that
3033 for rlo in rl_outputs:
3034 pass_used = False # initial value. Will be set to True if pass is used
3035 if out.name == 'Alpha':
3036 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3037 pass_used = True
3038 elif out.name in {rlo.output_name, rlo.exr_output_name}:
3039 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3040 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
3041 break
3042 if pass_used:
3043 outputs.append(out)
3044 doit = True # Will be changed to False when links successfully added to previous output.
3045 for out in outputs:
3046 if doit:
3047 for node in selected:
3048 dst_name = node.name # Will be compared with src_name if needed.
3049 # When node has label - use it as dst_name
3050 if node.label:
3051 dst_name = node.label
3052 valid = True # Initial value. Will be changed to False if names don't match.
3053 src_name = dst_name # If names not used - this asignment will keep valid = True.
3054 if use_node_name:
3055 # Set src_name to source node name or label
3056 src_name = active.name
3057 if active.label:
3058 src_name = active.label
3059 elif use_outputs_names:
3060 src_name = (out.name, )
3061 for rlo in rl_outputs:
3062 if out.name in {rlo.output_name, rlo.exr_output_name}:
3063 src_name = (rlo.output_name, rlo.exr_output_name)
3064 if dst_name not in src_name:
3065 valid = False
3066 if valid:
3067 for input in node.inputs:
3068 if input.type == out.type or node.type == 'REROUTE':
3069 if replace or not input.is_linked:
3070 links.new(out, input)
3071 if not use_node_name and not use_outputs_names:
3072 doit = False
3073 break
3075 return {'FINISHED'}
3078 class NWAlignNodes(Operator, NWBase):
3079 '''Align the selected nodes neatly in a row/column'''
3080 bl_idname = "node.nw_align_nodes"
3081 bl_label = "Align Nodes"
3082 bl_options = {'REGISTER', 'UNDO'}
3083 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3085 def execute(self, context):
3086 nodes, links = get_nodes_links(context)
3087 margin = self.margin
3089 selection = []
3090 for node in nodes:
3091 if node.select and node.type != 'FRAME':
3092 selection.append(node)
3094 # If no nodes are selected, align all nodes
3095 active_loc = None
3096 if not selection:
3097 selection = nodes
3098 elif nodes.active in selection:
3099 active_loc = copy(nodes.active.location) # make a copy, not a reference
3101 # Check if nodes should be laid out horizontally or vertically
3102 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3103 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3104 x_range = max(x_locs) - min(x_locs)
3105 y_range = max(y_locs) - min(y_locs)
3106 mid_x = (max(x_locs) + min(x_locs)) / 2
3107 mid_y = (max(y_locs) + min(y_locs)) / 2
3108 horizontal = x_range > y_range
3110 # Sort selection by location of node mid-point
3111 if horizontal:
3112 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3113 else:
3114 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3116 # Alignment
3117 current_pos = 0
3118 for node in selection:
3119 current_margin = margin
3120 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3122 if horizontal:
3123 node.location.x = current_pos
3124 current_pos += current_margin + node.dimensions.x
3125 node.location.y = mid_y + (node.dimensions.y / 2)
3126 else:
3127 node.location.y = current_pos
3128 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3129 node.location.x = mid_x - (node.dimensions.x / 2)
3131 # If active node is selected, center nodes around it
3132 if active_loc is not None:
3133 active_loc_diff = active_loc - nodes.active.location
3134 for node in selection:
3135 node.location += active_loc_diff
3136 else: # Position nodes centered around where they used to be
3137 locs = ([n.location.x + (n.dimensions.x / 2) for n in selection]) if horizontal else ([n.location.y - (n.dimensions.y / 2) for n in selection])
3138 new_mid = (max(locs) + min(locs)) / 2
3139 for node in selection:
3140 if horizontal:
3141 node.location.x += (mid_x - new_mid)
3142 else:
3143 node.location.y += (mid_y - new_mid)
3145 return {'FINISHED'}
3148 class NWSelectParentChildren(Operator, NWBase):
3149 bl_idname = "node.nw_select_parent_child"
3150 bl_label = "Select Parent or Children"
3151 bl_options = {'REGISTER', 'UNDO'}
3153 option: EnumProperty(
3154 name="option",
3155 items=(
3156 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3157 ('CHILD', 'Select Children', 'Select members of selected frame'),
3161 def execute(self, context):
3162 nodes, links = get_nodes_links(context)
3163 option = self.option
3164 selected = [node for node in nodes if node.select]
3165 if option == 'PARENT':
3166 for sel in selected:
3167 parent = sel.parent
3168 if parent:
3169 parent.select = True
3170 else: # option == 'CHILD'
3171 for sel in selected:
3172 children = [node for node in nodes if node.parent == sel]
3173 for kid in children:
3174 kid.select = True
3176 return {'FINISHED'}
3179 class NWDetachOutputs(Operator, NWBase):
3180 """Detach outputs of selected node leaving inputs linked"""
3181 bl_idname = "node.nw_detach_outputs"
3182 bl_label = "Detach Outputs"
3183 bl_options = {'REGISTER', 'UNDO'}
3185 def execute(self, context):
3186 nodes, links = get_nodes_links(context)
3187 selected = context.selected_nodes
3188 bpy.ops.node.duplicate_move_keep_inputs()
3189 new_nodes = context.selected_nodes
3190 bpy.ops.node.select_all(action="DESELECT")
3191 for node in selected:
3192 node.select = True
3193 bpy.ops.node.delete_reconnect()
3194 for new_node in new_nodes:
3195 new_node.select = True
3196 bpy.ops.transform.translate('INVOKE_DEFAULT')
3198 return {'FINISHED'}
3201 class NWLinkToOutputNode(Operator, NWBase):
3202 """Link to Composite node or Material Output node"""
3203 bl_idname = "node.nw_link_out"
3204 bl_label = "Connect to Output"
3205 bl_options = {'REGISTER', 'UNDO'}
3207 @classmethod
3208 def poll(cls, context):
3209 valid = False
3210 if nw_check(context):
3211 if context.active_node is not None:
3212 for out in context.active_node.outputs:
3213 if not out.hide:
3214 valid = True
3215 break
3216 return valid
3218 def execute(self, context):
3219 nodes, links = get_nodes_links(context)
3220 active = nodes.active
3221 output_node = None
3222 output_index = None
3223 tree_type = context.space_data.tree_type
3224 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
3225 output_types_compo = ['COMPOSITE']
3226 output_types_blender_mat = ['OUTPUT']
3227 output_types_textures = ['OUTPUT']
3228 output_types = output_types_shaders + output_types_compo + output_types_blender_mat
3229 for node in nodes:
3230 if node.type in output_types:
3231 output_node = node
3232 break
3233 if not output_node:
3234 bpy.ops.node.select_all(action="DESELECT")
3235 if tree_type == 'ShaderNodeTree':
3236 if is_cycles_or_eevee(context):
3237 output_node = nodes.new('ShaderNodeOutputMaterial')
3238 else:
3239 output_node = nodes.new('ShaderNodeOutput')
3240 elif tree_type == 'CompositorNodeTree':
3241 output_node = nodes.new('CompositorNodeComposite')
3242 elif tree_type == 'TextureNodeTree':
3243 output_node = nodes.new('TextureNodeOutput')
3244 output_node.location.x = active.location.x + active.dimensions.x + 80
3245 output_node.location.y = active.location.y
3246 if (output_node and active.outputs):
3247 for i, output in enumerate(active.outputs):
3248 if not output.hide:
3249 output_index = i
3250 break
3251 for i, output in enumerate(active.outputs):
3252 if output.type == output_node.inputs[0].type and not output.hide:
3253 output_index = i
3254 break
3256 out_input_index = 0
3257 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3258 if active.outputs[output_index].name == 'Volume':
3259 out_input_index = 1
3260 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3261 out_input_index = 2
3262 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3264 force_update(context) # viewport render does not update
3266 return {'FINISHED'}
3269 class NWMakeLink(Operator, NWBase):
3270 """Make a link from one socket to another"""
3271 bl_idname = 'node.nw_make_link'
3272 bl_label = 'Make Link'
3273 bl_options = {'REGISTER', 'UNDO'}
3274 from_socket: IntProperty()
3275 to_socket: IntProperty()
3277 def execute(self, context):
3278 nodes, links = get_nodes_links(context)
3280 n1 = nodes[context.scene.NWLazySource]
3281 n2 = nodes[context.scene.NWLazyTarget]
3283 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3285 force_update(context)
3287 return {'FINISHED'}
3290 class NWCallInputsMenu(Operator, NWBase):
3291 """Link from this output"""
3292 bl_idname = 'node.nw_call_inputs_menu'
3293 bl_label = 'Make Link'
3294 bl_options = {'REGISTER', 'UNDO'}
3295 from_socket: IntProperty()
3297 def execute(self, context):
3298 nodes, links = get_nodes_links(context)
3300 context.scene.NWSourceSocket = self.from_socket
3302 n1 = nodes[context.scene.NWLazySource]
3303 n2 = nodes[context.scene.NWLazyTarget]
3304 if len(n2.inputs) > 1:
3305 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3306 elif len(n2.inputs) == 1:
3307 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3308 return {'FINISHED'}
3311 class NWAddSequence(Operator, NWBase, ImportHelper):
3312 """Add an Image Sequence"""
3313 bl_idname = 'node.nw_add_sequence'
3314 bl_label = 'Import Image Sequence'
3315 bl_options = {'REGISTER', 'UNDO'}
3317 directory: StringProperty(
3318 subtype="DIR_PATH"
3320 filename: StringProperty(
3321 subtype="FILE_NAME"
3323 files: CollectionProperty(
3324 type=bpy.types.OperatorFileListElement,
3325 options={'HIDDEN', 'SKIP_SAVE'}
3328 def execute(self, context):
3329 nodes, links = get_nodes_links(context)
3330 directory = self.directory
3331 filename = self.filename
3332 files = self.files
3333 tree = context.space_data.node_tree
3335 # DEBUG
3336 # print ("\nDIR:", directory)
3337 # print ("FN:", filename)
3338 # print ("Fs:", list(f.name for f in files), '\n')
3340 if tree.type == 'SHADER':
3341 node_type = "ShaderNodeTexImage"
3342 elif tree.type == 'COMPOSITING':
3343 node_type = "CompositorNodeImage"
3344 else:
3345 self.report({'ERROR'}, "Unsupported Node Tree type!")
3346 return {'CANCELLED'}
3348 if not files[0].name and not filename:
3349 self.report({'ERROR'}, "No file chosen")
3350 return {'CANCELLED'}
3351 elif files[0].name and (not filename or not path.exists(directory+filename)):
3352 # User has selected multiple files without an active one, or the active one is non-existant
3353 filename = files[0].name
3355 if not path.exists(directory+filename):
3356 self.report({'ERROR'}, filename+" does not exist!")
3357 return {'CANCELLED'}
3359 without_ext = '.'.join(filename.split('.')[:-1])
3361 # if last digit isn't a number, it's not a sequence
3362 if not without_ext[-1].isdigit():
3363 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3364 return {'CANCELLED'}
3367 extension = filename.split('.')[-1]
3368 reverse = without_ext[::-1] # reverse string
3370 count_numbers = 0
3371 for char in reverse:
3372 if char.isdigit():
3373 count_numbers += 1
3374 else:
3375 break
3377 without_num = without_ext[:count_numbers*-1]
3379 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3381 num_frames = len(files)
3383 nodes_list = [node for node in nodes]
3384 if nodes_list:
3385 nodes_list.sort(key=lambda k: k.location.x)
3386 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3387 yloc = 0
3388 for node in nodes:
3389 node.select = False
3390 yloc += node_mid_pt(node, 'y')
3391 yloc = yloc/len(nodes)
3392 else:
3393 xloc = 0
3394 yloc = 0
3396 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3398 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3399 node = nodes.active
3400 node.label = name_with_hashes
3402 img = bpy.data.images.load(directory+(without_ext+'.'+extension))
3403 img.source = 'SEQUENCE'
3404 img.name = name_with_hashes
3405 node.image = img
3406 image_user = node.image_user if tree.type == 'SHADER' else node
3407 image_user.frame_offset = int(files[0][len(without_num)+len(directory):-1*(len(extension)+1)]) - 1 # separate the number from the file name of the first file
3408 image_user.frame_duration = num_frames
3410 return {'FINISHED'}
3413 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
3414 """Add multiple images at once"""
3415 bl_idname = 'node.nw_add_multiple_images'
3416 bl_label = 'Open Selected Images'
3417 bl_options = {'REGISTER', 'UNDO'}
3418 directory: StringProperty(
3419 subtype="DIR_PATH"
3421 files: CollectionProperty(
3422 type=bpy.types.OperatorFileListElement,
3423 options={'HIDDEN', 'SKIP_SAVE'}
3426 def execute(self, context):
3427 nodes, links = get_nodes_links(context)
3429 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3431 if context.space_data.node_tree.type == 'SHADER':
3432 node_type = "ShaderNodeTexImage"
3433 elif context.space_data.node_tree.type == 'COMPOSITING':
3434 node_type = "CompositorNodeImage"
3435 else:
3436 self.report({'ERROR'}, "Unsupported Node Tree type!")
3437 return {'CANCELLED'}
3439 new_nodes = []
3440 for f in self.files:
3441 fname = f.name
3443 node = nodes.new(node_type)
3444 new_nodes.append(node)
3445 node.label = fname
3446 node.hide = True
3447 node.width_hidden = 100
3448 node.location.x = xloc
3449 node.location.y = yloc
3450 yloc -= 40
3452 img = bpy.data.images.load(self.directory+fname)
3453 node.image = img
3455 # shift new nodes up to center of tree
3456 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3457 for node in nodes:
3458 if node in new_nodes:
3459 node.select = True
3460 node.location.y += (list_size/2)
3461 else:
3462 node.select = False
3463 return {'FINISHED'}
3466 class NWViewerFocus(bpy.types.Operator):
3467 """Set the viewer tile center to the mouse position"""
3468 bl_idname = "node.nw_viewer_focus"
3469 bl_label = "Viewer Focus"
3471 x: bpy.props.IntProperty()
3472 y: bpy.props.IntProperty()
3474 @classmethod
3475 def poll(cls, context):
3476 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3478 def execute(self, context):
3479 return {'FINISHED'}
3481 def invoke(self, context, event):
3482 render = context.scene.render
3483 space = context.space_data
3484 percent = render.resolution_percentage*0.01
3486 nodes, links = get_nodes_links(context)
3487 viewers = [n for n in nodes if n.type == 'VIEWER']
3489 if viewers:
3490 mlocx = event.mouse_region_x
3491 mlocy = event.mouse_region_y
3492 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
3494 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3495 region_x = context.region.width
3496 region_y = context.region.height
3498 region_center_x = context.region.width / 2
3499 region_center_y = context.region.height / 2
3501 bd_x = render.resolution_x * percent * space.backdrop_zoom
3502 bd_y = render.resolution_y * percent * space.backdrop_zoom
3504 backdrop_center_x = (bd_x / 2) - space.backdrop_x
3505 backdrop_center_y = (bd_y / 2) - space.backdrop_y
3507 margin_x = region_center_x - backdrop_center_x
3508 margin_y = region_center_y - backdrop_center_y
3510 abs_mouse_x = (mlocx - margin_x) / bd_x
3511 abs_mouse_y = (mlocy - margin_y) / bd_y
3513 for node in viewers:
3514 node.center_x = abs_mouse_x
3515 node.center_y = abs_mouse_y
3516 else:
3517 return {'PASS_THROUGH'}
3519 return self.execute(context)
3522 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3523 """Save the current viewer node to an image file"""
3524 bl_idname = "node.nw_save_viewer"
3525 bl_label = "Save This Image"
3526 filepath: StringProperty(subtype="FILE_PATH")
3527 filename_ext: EnumProperty(
3528 name="Format",
3529 description="Choose the file format to save to",
3530 items=(('.bmp', "PNG", ""),
3531 ('.rgb', 'IRIS', ""),
3532 ('.png', 'PNG', ""),
3533 ('.jpg', 'JPEG', ""),
3534 ('.jp2', 'JPEG2000', ""),
3535 ('.tga', 'TARGA', ""),
3536 ('.cin', 'CINEON', ""),
3537 ('.dpx', 'DPX', ""),
3538 ('.exr', 'OPEN_EXR', ""),
3539 ('.hdr', 'HDR', ""),
3540 ('.tif', 'TIFF', "")),
3541 default='.png',
3544 @classmethod
3545 def poll(cls, context):
3546 valid = False
3547 if nw_check(context):
3548 if context.space_data.tree_type == 'CompositorNodeTree':
3549 if "Viewer Node" in [i.name for i in bpy.data.images]:
3550 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3551 valid = True
3552 return valid
3554 def execute(self, context):
3555 fp = self.filepath
3556 if fp:
3557 formats = {
3558 '.bmp': 'BMP',
3559 '.rgb': 'IRIS',
3560 '.png': 'PNG',
3561 '.jpg': 'JPEG',
3562 '.jpeg': 'JPEG',
3563 '.jp2': 'JPEG2000',
3564 '.tga': 'TARGA',
3565 '.cin': 'CINEON',
3566 '.dpx': 'DPX',
3567 '.exr': 'OPEN_EXR',
3568 '.hdr': 'HDR',
3569 '.tiff': 'TIFF',
3570 '.tif': 'TIFF'}
3571 basename, ext = path.splitext(fp)
3572 old_render_format = context.scene.render.image_settings.file_format
3573 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3574 context.area.type = "IMAGE_EDITOR"
3575 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3576 context.area.spaces[0].image.save_render(fp)
3577 context.area.type = "NODE_EDITOR"
3578 context.scene.render.image_settings.file_format = old_render_format
3579 return {'FINISHED'}
3582 class NWResetNodes(bpy.types.Operator):
3583 """Reset Nodes in Selection"""
3584 bl_idname = "node.nw_reset_nodes"
3585 bl_label = "Reset Nodes"
3586 bl_options = {'REGISTER', 'UNDO'}
3588 @classmethod
3589 def poll(cls, context):
3590 space = context.space_data
3591 return space.type == 'NODE_EDITOR'
3593 def execute(self, context):
3594 node_active = context.active_node
3595 node_selected = context.selected_nodes
3596 node_ignore = ["FRAME","REROUTE", "GROUP"]
3598 # Check if one node is selected at least
3599 if not (len(node_selected) > 0):
3600 self.report({'ERROR'}, "1 node must be selected at least")
3601 return {'CANCELLED'}
3603 active_node_name = node_active.name if node_active.select else None
3604 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3606 # Create output lists
3607 selected_node_names = [n.name for n in node_selected]
3608 success_names = []
3610 # Reset all valid children in a frame
3611 node_active_is_frame = False
3612 if len(node_selected) == 1 and node_active.type == "FRAME":
3613 node_tree = node_active.id_data
3614 children = [n for n in node_tree.nodes if n.parent == node_active]
3615 if children:
3616 valid_nodes = [n for n in children if n.type not in node_ignore]
3617 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3618 node_active_is_frame = True
3620 # Check if valid nodes in selection
3621 if not (len(valid_nodes) > 0):
3622 # Check for frames only
3623 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3624 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3625 self.report({'ERROR'}, "Please select only 1 frame to reset")
3626 else:
3627 self.report({'ERROR'}, "No valid node(s) in selection")
3628 return {'CANCELLED'}
3630 # Report nodes that are not valid
3631 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3632 valid_node_names = [n.name for n in valid_nodes]
3633 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3634 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3636 # Deselect all nodes
3637 for i in node_selected:
3638 i.select = False
3640 # Run through all valid nodes
3641 for node in valid_nodes:
3643 parent = node.parent if node.parent else None
3644 node_loc = [node.location.x, node.location.y]
3646 node_tree = node.id_data
3647 props_to_copy = 'bl_idname name location height width'.split(' ')
3649 reconnections = []
3650 mappings = chain.from_iterable([node.inputs, node.outputs])
3651 for i in (i for i in mappings if i.is_linked):
3652 for L in i.links:
3653 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3655 props = {j: getattr(node, j) for j in props_to_copy}
3657 new_node = node_tree.nodes.new(props['bl_idname'])
3658 props_to_copy.pop(0)
3660 for prop in props_to_copy:
3661 setattr(new_node, prop, props[prop])
3663 nodes = node_tree.nodes
3664 nodes.remove(node)
3665 new_node.name = props['name']
3667 if parent:
3668 new_node.parent = parent
3669 new_node.location = node_loc
3671 for str_from, str_to in reconnections:
3672 node_tree.links.new(eval(str_from), eval(str_to))
3674 new_node.select = False
3675 success_names.append(new_node.name)
3677 # Reselect all nodes
3678 if selected_node_names and node_active_is_frame is False:
3679 for i in selected_node_names:
3680 node_tree.nodes[i].select = True
3682 if active_node_name is not None:
3683 node_tree.nodes[active_node_name].select = True
3684 node_tree.nodes.active = node_tree.nodes[active_node_name]
3686 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3687 return {'FINISHED'}
3691 # P A N E L
3694 def drawlayout(context, layout, mode='non-panel'):
3695 tree_type = context.space_data.tree_type
3697 col = layout.column(align=True)
3698 col.menu(NWMergeNodesMenu.bl_idname)
3699 col.separator()
3701 col = layout.column(align=True)
3702 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3703 col.separator()
3705 if tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3706 col = layout.column(align=True)
3707 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3708 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3709 col.separator()
3711 col = layout.column(align=True)
3712 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3713 col.operator(NWSwapLinks.bl_idname)
3714 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3715 col.separator()
3717 col = layout.column(align=True)
3718 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3719 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3720 col.separator()
3722 col = layout.column(align=True)
3723 if mode == 'panel':
3724 row = col.row(align=True)
3725 row.operator(NWClearLabel.bl_idname).option = True
3726 row.operator(NWModifyLabels.bl_idname)
3727 else:
3728 col.operator(NWClearLabel.bl_idname).option = True
3729 col.operator(NWModifyLabels.bl_idname)
3730 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3731 col.separator()
3732 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3733 col.separator()
3735 col = layout.column(align=True)
3736 if tree_type == 'CompositorNodeTree':
3737 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
3738 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
3739 col.separator()
3741 col = layout.column(align=True)
3742 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3743 col.separator()
3745 col = layout.column(align=True)
3746 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
3747 col.separator()
3749 col = layout.column(align=True)
3750 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3751 col.separator()
3754 class NodeWranglerPanel(Panel, NWBase):
3755 bl_idname = "NODE_PT_nw_node_wrangler"
3756 bl_space_type = 'NODE_EDITOR'
3757 bl_label = "Node Wrangler"
3758 bl_region_type = "UI"
3759 bl_category = "Node Wrangler"
3761 prepend: StringProperty(
3762 name='prepend',
3764 append: StringProperty()
3765 remove: StringProperty()
3767 def draw(self, context):
3768 self.layout.label(text="(Quick access: Ctrl+Space)")
3769 drawlayout(context, self.layout, mode='panel')
3773 # M E N U S
3775 class NodeWranglerMenu(Menu, NWBase):
3776 bl_idname = "NODE_MT_nw_node_wrangler_menu"
3777 bl_label = "Node Wrangler"
3779 def draw(self, context):
3780 drawlayout(context, self.layout)
3783 class NWMergeNodesMenu(Menu, NWBase):
3784 bl_idname = "NODE_MT_nw_merge_nodes_menu"
3785 bl_label = "Merge Selected Nodes"
3787 def draw(self, context):
3788 type = context.space_data.tree_type
3789 layout = self.layout
3790 if type == 'ShaderNodeTree' and is_cycles_or_eevee(context):
3791 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
3792 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
3793 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3794 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
3795 props.mode = 'MIX'
3796 props.merge_type = 'ZCOMBINE'
3797 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3798 props.mode = 'MIX'
3799 props.merge_type = 'ALPHAOVER'
3802 class NWMergeShadersMenu(Menu, NWBase):
3803 bl_idname = "NODE_MT_nw_merge_shaders_menu"
3804 bl_label = "Merge Selected Nodes using Shaders"
3806 def draw(self, context):
3807 layout = self.layout
3808 for type in ('MIX', 'ADD'):
3809 props = layout.operator(NWMergeNodes.bl_idname, text=type)
3810 props.mode = type
3811 props.merge_type = 'SHADER'
3814 class NWMergeMixMenu(Menu, NWBase):
3815 bl_idname = "NODE_MT_nw_merge_mix_menu"
3816 bl_label = "Merge Selected Nodes using Mix"
3818 def draw(self, context):
3819 layout = self.layout
3820 for type, name, description in blend_types:
3821 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3822 props.mode = type
3823 props.merge_type = 'MIX'
3826 class NWConnectionListOutputs(Menu, NWBase):
3827 bl_idname = "NODE_MT_nw_connection_list_out"
3828 bl_label = "From:"
3830 def draw(self, context):
3831 layout = self.layout
3832 nodes, links = get_nodes_links(context)
3834 n1 = nodes[context.scene.NWLazySource]
3836 if n1.type == "R_LAYERS":
3837 index=0
3838 for o in n1.outputs:
3839 if o.enabled: # Check which passes the render layer has enabled
3840 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3841 index+=1
3842 else:
3843 index=0
3844 for o in n1.outputs:
3845 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
3846 index+=1
3849 class NWConnectionListInputs(Menu, NWBase):
3850 bl_idname = "NODE_MT_nw_connection_list_in"
3851 bl_label = "To:"
3853 def draw(self, context):
3854 layout = self.layout
3855 nodes, links = get_nodes_links(context)
3857 n2 = nodes[context.scene.NWLazyTarget]
3859 index = 0
3860 for i in n2.inputs:
3861 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
3862 op.from_socket = context.scene.NWSourceSocket
3863 op.to_socket = index
3864 index+=1
3867 class NWMergeMathMenu(Menu, NWBase):
3868 bl_idname = "NODE_MT_nw_merge_math_menu"
3869 bl_label = "Merge Selected Nodes using Math"
3871 def draw(self, context):
3872 layout = self.layout
3873 for type, name, description in operations:
3874 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3875 props.mode = type
3876 props.merge_type = 'MATH'
3879 class NWBatchChangeNodesMenu(Menu, NWBase):
3880 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3881 bl_label = "Batch Change Selected Nodes"
3883 def draw(self, context):
3884 layout = self.layout
3885 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3886 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3889 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3890 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3891 bl_label = "Batch Change Blend Type"
3893 def draw(self, context):
3894 layout = self.layout
3895 for type, name, description in blend_types:
3896 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3897 props.blend_type = type
3898 props.operation = 'CURRENT'
3901 class NWBatchChangeOperationMenu(Menu, NWBase):
3902 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3903 bl_label = "Batch Change Math Operation"
3905 def draw(self, context):
3906 layout = self.layout
3907 for type, name, description in operations:
3908 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3909 props.blend_type = 'CURRENT'
3910 props.operation = type
3913 class NWCopyToSelectedMenu(Menu, NWBase):
3914 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
3915 bl_label = "Copy to Selected"
3917 def draw(self, context):
3918 layout = self.layout
3919 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
3920 layout.menu(NWCopyLabelMenu.bl_idname)
3923 class NWCopyLabelMenu(Menu, NWBase):
3924 bl_idname = "NODE_MT_nw_copy_label_menu"
3925 bl_label = "Copy Label"
3927 def draw(self, context):
3928 layout = self.layout
3929 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
3930 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
3931 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
3934 class NWAddReroutesMenu(Menu, NWBase):
3935 bl_idname = "NODE_MT_nw_add_reroutes_menu"
3936 bl_label = "Add Reroutes"
3937 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
3939 def draw(self, context):
3940 layout = self.layout
3941 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
3942 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
3943 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
3946 class NWLinkActiveToSelectedMenu(Menu, NWBase):
3947 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
3948 bl_label = "Link Active to Selected"
3950 def draw(self, context):
3951 layout = self.layout
3952 layout.menu(NWLinkStandardMenu.bl_idname)
3953 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
3954 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
3957 class NWLinkStandardMenu(Menu, NWBase):
3958 bl_idname = "NODE_MT_nw_link_standard_menu"
3959 bl_label = "To All Selected"
3961 def draw(self, context):
3962 layout = self.layout
3963 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3964 props.replace = False
3965 props.use_node_name = False
3966 props.use_outputs_names = False
3967 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3968 props.replace = True
3969 props.use_node_name = False
3970 props.use_outputs_names = False
3973 class NWLinkUseNodeNameMenu(Menu, NWBase):
3974 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
3975 bl_label = "Use Node Name/Label"
3977 def draw(self, context):
3978 layout = self.layout
3979 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3980 props.replace = False
3981 props.use_node_name = True
3982 props.use_outputs_names = False
3983 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
3984 props.replace = True
3985 props.use_node_name = True
3986 props.use_outputs_names = False
3989 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
3990 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
3991 bl_label = "Use Outputs Names"
3993 def draw(self, context):
3994 layout = self.layout
3995 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
3996 props.replace = False
3997 props.use_node_name = False
3998 props.use_outputs_names = True
3999 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4000 props.replace = True
4001 props.use_node_name = False
4002 props.use_outputs_names = True
4005 class NWVertColMenu(bpy.types.Menu):
4006 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
4007 bl_label = "Vertex Colors"
4009 @classmethod
4010 def poll(cls, context):
4011 valid = False
4012 if nw_check(context):
4013 snode = context.space_data
4014 valid = snode.tree_type == 'ShaderNodeTree' and is_cycles_or_eevee(context)
4015 return valid
4017 def draw(self, context):
4018 l = self.layout
4019 nodes, links = get_nodes_links(context)
4020 mat = context.object.active_material
4022 objs = []
4023 for obj in bpy.data.objects:
4024 for slot in obj.material_slots:
4025 if slot.material == mat:
4026 objs.append(obj)
4027 vcols = []
4028 for obj in objs:
4029 if obj.data.vertex_colors:
4030 for vcol in obj.data.vertex_colors:
4031 vcols.append(vcol.name)
4032 vcols = list(set(vcols)) # get a unique list
4034 if vcols:
4035 for vcol in vcols:
4036 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
4037 else:
4038 l.label("No Vertex Color layers on objects with this material")
4041 class NWSwitchNodeTypeMenu(Menu, NWBase):
4042 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4043 bl_label = "Switch Type to..."
4045 def draw(self, context):
4046 layout = self.layout
4047 tree = context.space_data.node_tree
4048 if tree.type == 'SHADER':
4049 if is_cycles_or_eevee(context):
4050 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
4051 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
4052 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
4053 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
4054 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
4055 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
4056 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
4057 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
4058 else:
4059 layout.menu(NWSwitchMatInputSubmenu.bl_idname)
4060 layout.menu(NWSwitchMatOutputSubmenu.bl_idname)
4061 layout.menu(NWSwitchMatColorSubmenu.bl_idname)
4062 layout.menu(NWSwitchMatVectorSubmenu.bl_idname)
4063 layout.menu(NWSwitchMatConverterSubmenu.bl_idname)
4064 layout.menu(NWSwitchMatLayoutSubmenu.bl_idname)
4065 if tree.type == 'COMPOSITING':
4066 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
4067 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
4068 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
4069 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
4070 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
4071 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
4072 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
4073 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
4074 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
4075 if tree.type == 'TEXTURE':
4076 layout.menu(NWSwitchTexInputSubmenu.bl_idname)
4077 layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
4078 layout.menu(NWSwitchTexColorSubmenu.bl_idname)
4079 layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
4080 layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
4081 layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
4082 layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
4083 layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
4086 class NWSwitchShadersInputSubmenu(Menu, NWBase):
4087 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
4088 bl_label = "Input"
4090 def draw(self, context):
4091 layout = self.layout
4092 for ident, node_type, rna_name in sorted(shaders_input_nodes_props, key=lambda k: k[2]):
4093 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4094 props.to_type = ident
4097 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
4098 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
4099 bl_label = "Output"
4101 def draw(self, context):
4102 layout = self.layout
4103 for ident, node_type, rna_name in sorted(shaders_output_nodes_props, key=lambda k: k[2]):
4104 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4105 props.to_type = ident
4108 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
4109 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
4110 bl_label = "Shader"
4112 def draw(self, context):
4113 layout = self.layout
4114 for ident, node_type, rna_name in sorted(shaders_shader_nodes_props, key=lambda k: k[2]):
4115 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4116 props.to_type = ident
4119 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
4120 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
4121 bl_label = "Texture"
4123 def draw(self, context):
4124 layout = self.layout
4125 for ident, node_type, rna_name in sorted(shaders_texture_nodes_props, key=lambda k: k[2]):
4126 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4127 props.to_type = ident
4130 class NWSwitchShadersColorSubmenu(Menu, NWBase):
4131 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
4132 bl_label = "Color"
4134 def draw(self, context):
4135 layout = self.layout
4136 for ident, node_type, rna_name in sorted(shaders_color_nodes_props, key=lambda k: k[2]):
4137 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4138 props.to_type = ident
4141 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
4142 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
4143 bl_label = "Vector"
4145 def draw(self, context):
4146 layout = self.layout
4147 for ident, node_type, rna_name in sorted(shaders_vector_nodes_props, key=lambda k: k[2]):
4148 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4149 props.to_type = ident
4152 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
4153 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
4154 bl_label = "Converter"
4156 def draw(self, context):
4157 layout = self.layout
4158 for ident, node_type, rna_name in sorted(shaders_converter_nodes_props, key=lambda k: k[2]):
4159 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4160 props.to_type = ident
4163 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
4164 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
4165 bl_label = "Layout"
4167 def draw(self, context):
4168 layout = self.layout
4169 for ident, node_type, rna_name in sorted(shaders_layout_nodes_props, key=lambda k: k[2]):
4170 if node_type != 'FRAME':
4171 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4172 props.to_type = ident
4175 class NWSwitchCompoInputSubmenu(Menu, NWBase):
4176 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
4177 bl_label = "Input"
4179 def draw(self, context):
4180 layout = self.layout
4181 for ident, node_type, rna_name in sorted(compo_input_nodes_props, key=lambda k: k[2]):
4182 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4183 props.to_type = ident
4186 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
4187 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
4188 bl_label = "Output"
4190 def draw(self, context):
4191 layout = self.layout
4192 for ident, node_type, rna_name in sorted(compo_output_nodes_props, key=lambda k: k[2]):
4193 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4194 props.to_type = ident
4197 class NWSwitchCompoColorSubmenu(Menu, NWBase):
4198 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
4199 bl_label = "Color"
4201 def draw(self, context):
4202 layout = self.layout
4203 for ident, node_type, rna_name in sorted(compo_color_nodes_props, key=lambda k: k[2]):
4204 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4205 props.to_type = ident
4208 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
4209 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
4210 bl_label = "Converter"
4212 def draw(self, context):
4213 layout = self.layout
4214 for ident, node_type, rna_name in sorted(compo_converter_nodes_props, key=lambda k: k[2]):
4215 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4216 props.to_type = ident
4219 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
4220 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
4221 bl_label = "Filter"
4223 def draw(self, context):
4224 layout = self.layout
4225 for ident, node_type, rna_name in sorted(compo_filter_nodes_props, key=lambda k: k[2]):
4226 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4227 props.to_type = ident
4230 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
4231 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
4232 bl_label = "Vector"
4234 def draw(self, context):
4235 layout = self.layout
4236 for ident, node_type, rna_name in sorted(compo_vector_nodes_props, key=lambda k: k[2]):
4237 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4238 props.to_type = ident
4241 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
4242 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
4243 bl_label = "Matte"
4245 def draw(self, context):
4246 layout = self.layout
4247 for ident, node_type, rna_name in sorted(compo_matte_nodes_props, key=lambda k: k[2]):
4248 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4249 props.to_type = ident
4252 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
4253 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
4254 bl_label = "Distort"
4256 def draw(self, context):
4257 layout = self.layout
4258 for ident, node_type, rna_name in sorted(compo_distort_nodes_props, key=lambda k: k[2]):
4259 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4260 props.to_type = ident
4263 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
4264 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
4265 bl_label = "Layout"
4267 def draw(self, context):
4268 layout = self.layout
4269 for ident, node_type, rna_name in sorted(compo_layout_nodes_props, key=lambda k: k[2]):
4270 if node_type != 'FRAME':
4271 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4272 props.to_type = ident
4275 class NWSwitchMatInputSubmenu(Menu, NWBase):
4276 bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
4277 bl_label = "Input"
4279 def draw(self, context):
4280 layout = self.layout
4281 for ident, node_type, rna_name in sorted(blender_mat_input_nodes_props, key=lambda k: k[2]):
4282 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4283 props.to_type = ident
4286 class NWSwitchMatOutputSubmenu(Menu, NWBase):
4287 bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
4288 bl_label = "Output"
4290 def draw(self, context):
4291 layout = self.layout
4292 for ident, node_type, rna_name in sorted(blender_mat_output_nodes_props, key=lambda k: k[2]):
4293 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4294 props.to_type = ident
4297 class NWSwitchMatColorSubmenu(Menu, NWBase):
4298 bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
4299 bl_label = "Color"
4301 def draw(self, context):
4302 layout = self.layout
4303 for ident, node_type, rna_name in sorted(blender_mat_color_nodes_props, key=lambda k: k[2]):
4304 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4305 props.to_type = ident
4308 class NWSwitchMatVectorSubmenu(Menu, NWBase):
4309 bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
4310 bl_label = "Vector"
4312 def draw(self, context):
4313 layout = self.layout
4314 for ident, node_type, rna_name in sorted(blender_mat_vector_nodes_props, key=lambda k: k[2]):
4315 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4316 props.to_type = ident
4319 class NWSwitchMatConverterSubmenu(Menu, NWBase):
4320 bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
4321 bl_label = "Converter"
4323 def draw(self, context):
4324 layout = self.layout
4325 for ident, node_type, rna_name in sorted(blender_mat_converter_nodes_props, key=lambda k: k[2]):
4326 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4327 props.to_type = ident
4330 class NWSwitchMatLayoutSubmenu(Menu, NWBase):
4331 bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
4332 bl_label = "Layout"
4334 def draw(self, context):
4335 layout = self.layout
4336 for ident, node_type, rna_name in sorted(blender_mat_layout_nodes_props, key=lambda k: k[2]):
4337 if node_type != 'FRAME':
4338 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4339 props.to_type = ident
4342 class NWSwitchTexInputSubmenu(Menu, NWBase):
4343 bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
4344 bl_label = "Input"
4346 def draw(self, context):
4347 layout = self.layout
4348 for ident, node_type, rna_name in sorted(texture_input_nodes_props, key=lambda k: k[2]):
4349 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4350 props.to_type = ident
4353 class NWSwitchTexOutputSubmenu(Menu, NWBase):
4354 bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
4355 bl_label = "Output"
4357 def draw(self, context):
4358 layout = self.layout
4359 for ident, node_type, rna_name in sorted(texture_output_nodes_props, key=lambda k: k[2]):
4360 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4361 props.to_type = ident
4364 class NWSwitchTexColorSubmenu(Menu, NWBase):
4365 bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
4366 bl_label = "Color"
4368 def draw(self, context):
4369 layout = self.layout
4370 for ident, node_type, rna_name in sorted(texture_color_nodes_props, key=lambda k: k[2]):
4371 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4372 props.to_type = ident
4375 class NWSwitchTexPatternSubmenu(Menu, NWBase):
4376 bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
4377 bl_label = "Pattern"
4379 def draw(self, context):
4380 layout = self.layout
4381 for ident, node_type, rna_name in sorted(texture_pattern_nodes_props, key=lambda k: k[2]):
4382 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4383 props.to_type = ident
4386 class NWSwitchTexTexturesSubmenu(Menu, NWBase):
4387 bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
4388 bl_label = "Textures"
4390 def draw(self, context):
4391 layout = self.layout
4392 for ident, node_type, rna_name in sorted(texture_textures_nodes_props, key=lambda k: k[2]):
4393 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4394 props.to_type = ident
4397 class NWSwitchTexConverterSubmenu(Menu, NWBase):
4398 bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
4399 bl_label = "Converter"
4401 def draw(self, context):
4402 layout = self.layout
4403 for ident, node_type, rna_name in sorted(texture_converter_nodes_props, key=lambda k: k[2]):
4404 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4405 props.to_type = ident
4408 class NWSwitchTexDistortSubmenu(Menu, NWBase):
4409 bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
4410 bl_label = "Distort"
4412 def draw(self, context):
4413 layout = self.layout
4414 for ident, node_type, rna_name in sorted(texture_distort_nodes_props, key=lambda k: k[2]):
4415 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4416 props.to_type = ident
4419 class NWSwitchTexLayoutSubmenu(Menu, NWBase):
4420 bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
4421 bl_label = "Layout"
4423 def draw(self, context):
4424 layout = self.layout
4425 for ident, node_type, rna_name in sorted(texture_layout_nodes_props, key=lambda k: k[2]):
4426 if node_type != 'FRAME':
4427 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4428 props.to_type = ident
4432 # APPENDAGES TO EXISTING UI
4436 def select_parent_children_buttons(self, context):
4437 layout = self.layout
4438 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4439 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4442 def attr_nodes_menu_func(self, context):
4443 col = self.layout.column(align=True)
4444 col.menu("NODE_MT_nw_node_vertex_color_menu")
4445 col.separator()
4448 def multipleimages_menu_func(self, context):
4449 col = self.layout.column(align=True)
4450 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4451 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4452 col.separator()
4455 def bgreset_menu_func(self, context):
4456 self.layout.operator(NWResetBG.bl_idname)
4459 def save_viewer_menu_func(self, context):
4460 if nw_check(context):
4461 if context.space_data.tree_type == 'CompositorNodeTree':
4462 if context.scene.node_tree.nodes.active:
4463 if context.scene.node_tree.nodes.active.type == "VIEWER":
4464 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4467 def reset_nodes_button(self, context):
4468 node_active = context.active_node
4469 node_selected = context.selected_nodes
4470 node_ignore = ["FRAME","REROUTE", "GROUP"]
4472 # Check if active node is in the selection and respective type
4473 if (len(node_selected) == 1) and node_active.select and node_active.type not in node_ignore:
4474 row = self.layout.row()
4475 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4476 self.layout.separator()
4478 elif (len(node_selected) == 1) and node_active.select and node_active.type == "FRAME":
4479 row = self.layout.row()
4480 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4481 self.layout.separator()
4485 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4488 addon_keymaps = []
4489 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4490 # props entry: (property name, property value)
4491 kmi_defs = (
4492 # MERGE NODES
4493 # NWMergeNodes with Ctrl (AUTO).
4494 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4495 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4496 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4497 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4498 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4499 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4500 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4501 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4502 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4503 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4504 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4505 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4506 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4507 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4508 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4509 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4510 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4511 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4512 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4513 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4514 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4515 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4516 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4517 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4518 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4519 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4520 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4521 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4522 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4523 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4524 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4525 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4526 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4527 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4528 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4529 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4530 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4531 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4532 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4533 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4534 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4535 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4536 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4537 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4538 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4539 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4540 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4541 # NWMergeNodes with Ctrl Shift (MATH)
4542 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4543 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4544 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4545 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4546 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4547 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4548 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4549 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4550 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4551 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4552 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4553 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4554 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4555 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4556 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4557 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4558 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4559 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4560 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4561 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4562 # BATCH CHANGE NODES
4563 # NWBatchChangeNodes with Alt
4564 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4565 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4566 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4567 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4568 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4569 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4570 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4571 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4572 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4573 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4574 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4575 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4576 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4577 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4578 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4579 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4580 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4581 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4582 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4583 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4584 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4585 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4586 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4587 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4588 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4589 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4590 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4591 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4592 # LINK ACTIVE TO SELECTED
4593 # Don't use names, don't replace links (K)
4594 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4595 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4596 # Don't use names, replace links (Shift K)
4597 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4598 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4599 # Use node name, don't replace links (')
4600 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4601 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4602 # Use node name, replace links (Shift ')
4603 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4604 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4605 # Don't use names, don't replace links (;)
4606 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4607 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4608 # Don't use names, replace links (')
4609 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4610 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4611 # CHANGE MIX FACTOR
4612 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4613 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4614 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4615 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4616 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4617 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4618 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4619 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4620 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4621 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4622 # CLEAR LABEL (Alt L)
4623 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4624 # MODIFY LABEL (Alt Shift L)
4625 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4626 # Copy Label from active to selected
4627 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4628 # DETACH OUTPUTS (Alt Shift D)
4629 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4630 # LINK TO OUTPUT NODE (O)
4631 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4632 # SELECT PARENT/CHILDREN
4633 # Select Children
4634 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4635 # Select Parent
4636 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4637 # Add Texture Setup
4638 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4639 # Add Principled BSDF Texture Setup
4640 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4641 # Reset backdrop
4642 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4643 # Delete unused
4644 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4645 # Frame Selected
4646 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4647 # Swap Outputs
4648 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4649 # Emission Viewer
4650 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4651 # Reload Images
4652 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4653 # Lazy Mix
4654 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, None, "Lazy Mix"),
4655 # Lazy Connect
4656 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, False, False, (('with_menu', False),), "Lazy Connect"),
4657 # Lazy Connect with Menu
4658 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4659 # Viewer Tile Center
4660 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4661 # Align Nodes
4662 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4663 # Reset Nodes (Back Space)
4664 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4665 # MENUS
4666 ('wm.call_menu', 'SPACE', 'PRESS', True, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
4667 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4668 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4669 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4670 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4671 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4675 classes = (
4676 NWPrincipledPreferences,
4677 NWNodeWrangler,
4678 NWLazyMix,
4679 NWLazyConnect,
4680 NWDeleteUnused,
4681 NWSwapLinks,
4682 NWResetBG,
4683 NWAddAttrNode,
4684 NWEmissionViewer,
4685 NWFrameSelected,
4686 NWReloadImages,
4687 NWSwitchNodeType,
4688 NWMergeNodes,
4689 NWBatchChangeNodes,
4690 NWChangeMixFactor,
4691 NWCopySettings,
4692 NWCopyLabel,
4693 NWClearLabel,
4694 NWModifyLabels,
4695 NWAddTextureSetup,
4696 NWAddPrincipledSetup,
4697 NWAddReroutes,
4698 NWLinkActiveToSelected,
4699 NWAlignNodes,
4700 NWSelectParentChildren,
4701 NWDetachOutputs,
4702 NWLinkToOutputNode,
4703 NWMakeLink,
4704 NWCallInputsMenu,
4705 NWAddSequence,
4706 NWAddMultipleImages,
4707 NWViewerFocus,
4708 NWSaveViewer,
4709 NWResetNodes,
4710 NodeWranglerPanel,
4711 NodeWranglerMenu,
4712 NWMergeNodesMenu,
4713 NWMergeShadersMenu,
4714 NWMergeMixMenu,
4715 NWConnectionListOutputs,
4716 NWConnectionListInputs,
4717 NWMergeMathMenu,
4718 NWBatchChangeNodesMenu,
4719 NWBatchChangeBlendTypeMenu,
4720 NWBatchChangeOperationMenu,
4721 NWCopyToSelectedMenu,
4722 NWCopyLabelMenu,
4723 NWAddReroutesMenu,
4724 NWLinkActiveToSelectedMenu,
4725 NWLinkStandardMenu,
4726 NWLinkUseNodeNameMenu,
4727 NWLinkUseOutputsNamesMenu,
4728 NWVertColMenu,
4729 NWSwitchNodeTypeMenu,
4730 NWSwitchShadersInputSubmenu,
4731 NWSwitchShadersOutputSubmenu,
4732 NWSwitchShadersShaderSubmenu,
4733 NWSwitchShadersTextureSubmenu,
4734 NWSwitchShadersColorSubmenu,
4735 NWSwitchShadersVectorSubmenu,
4736 NWSwitchShadersConverterSubmenu,
4737 NWSwitchShadersLayoutSubmenu,
4738 NWSwitchCompoInputSubmenu,
4739 NWSwitchCompoOutputSubmenu,
4740 NWSwitchCompoColorSubmenu,
4741 NWSwitchCompoConverterSubmenu,
4742 NWSwitchCompoFilterSubmenu,
4743 NWSwitchCompoVectorSubmenu,
4744 NWSwitchCompoMatteSubmenu,
4745 NWSwitchCompoDistortSubmenu,
4746 NWSwitchCompoLayoutSubmenu,
4747 NWSwitchMatInputSubmenu,
4748 NWSwitchMatOutputSubmenu,
4749 NWSwitchMatColorSubmenu,
4750 NWSwitchMatVectorSubmenu,
4751 NWSwitchMatConverterSubmenu,
4752 NWSwitchMatLayoutSubmenu,
4753 NWSwitchTexInputSubmenu,
4754 NWSwitchTexOutputSubmenu,
4755 NWSwitchTexColorSubmenu,
4756 NWSwitchTexPatternSubmenu,
4757 NWSwitchTexTexturesSubmenu,
4758 NWSwitchTexConverterSubmenu,
4759 NWSwitchTexDistortSubmenu,
4760 NWSwitchTexLayoutSubmenu,
4763 def register():
4764 from bpy.utils import register_class
4766 # props
4767 bpy.types.Scene.NWBusyDrawing = StringProperty(
4768 name="Busy Drawing!",
4769 default="",
4770 description="An internal property used to store only the first mouse position")
4771 bpy.types.Scene.NWLazySource = StringProperty(
4772 name="Lazy Source!",
4773 default="x",
4774 description="An internal property used to store the first node in a Lazy Connect operation")
4775 bpy.types.Scene.NWLazyTarget = StringProperty(
4776 name="Lazy Target!",
4777 default="x",
4778 description="An internal property used to store the last node in a Lazy Connect operation")
4779 bpy.types.Scene.NWSourceSocket = IntProperty(
4780 name="Source Socket!",
4781 default=0,
4782 description="An internal property used to store the source socket in a Lazy Connect operation")
4784 for cls in classes:
4785 register_class(cls)
4787 # keymaps
4788 addon_keymaps.clear()
4789 kc = bpy.context.window_manager.keyconfigs.addon
4790 if kc:
4791 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
4792 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
4793 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
4794 if props:
4795 for prop, value in props:
4796 setattr(kmi.properties, prop, value)
4797 addon_keymaps.append((km, kmi))
4799 # menu items
4800 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
4801 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4802 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
4803 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
4804 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4805 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4806 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
4807 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
4810 def unregister():
4811 from bpy.utils import unregister_class
4813 # props
4814 del bpy.types.Scene.NWBusyDrawing
4815 del bpy.types.Scene.NWLazySource
4816 del bpy.types.Scene.NWLazyTarget
4817 del bpy.types.Scene.NWSourceSocket
4819 # keymaps
4820 for km, kmi in addon_keymaps:
4821 km.keymap_items.remove(kmi)
4822 addon_keymaps.clear()
4824 # menuitems
4825 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
4826 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4827 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
4828 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
4829 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4830 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
4831 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
4832 bpy.types.NODE_MT_node.remove(reset_nodes_button)
4834 for cls in classes:
4835 unregister_class(cls)
4837 if __name__ == "__main__":
4838 register()