Cleanup: simplify file name incrementing logic
[blender-addons.git] / node_wrangler.py
blobe9ba5e742dfcedd80770c38cda5a7ab3869f8cd3
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, 37),
23 "blender": (2, 83, 0),
24 "location": "Node Editor Toolbar or Shift-W",
25 "description": "Various tools to enhance and speed up node-based workflow",
26 "warning": "",
27 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
28 "category": "Node",
31 import bpy, blf, bgl
32 import gpu
33 from bpy.types import Operator, Panel, Menu
34 from bpy.props import (
35 FloatProperty,
36 EnumProperty,
37 BoolProperty,
38 IntProperty,
39 StringProperty,
40 FloatVectorProperty,
41 CollectionProperty,
43 from bpy_extras.io_utils import ImportHelper, ExportHelper
44 from gpu_extras.batch import batch_for_shader
45 from mathutils import Vector
46 from math import cos, sin, pi, hypot
47 from os import path
48 from glob import glob
49 from copy import copy
50 from itertools import chain
51 import re
52 from collections import namedtuple
54 #################
55 # rl_outputs:
56 # list of outputs of Input Render Layer
57 # with attributes determinig if pass is used,
58 # and MultiLayer EXR outputs names and corresponding render engines
60 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
61 RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
62 rl_outputs = (
63 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
64 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
65 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
66 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
67 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
68 RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
69 RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
70 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
71 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
72 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
73 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
74 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
75 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
76 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
77 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
78 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
79 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
80 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
81 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
82 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
83 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
84 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
85 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
86 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
87 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
90 # shader nodes
91 # (rna_type.identifier, type, rna_type.name)
92 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
93 # Keeping things in alphabetical orde so we don't need to sort later.
94 shaders_input_nodes_props = (
95 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
96 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
97 ('ShaderNodeBevel', 'BEVEL', 'Bevel'),
98 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
99 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
100 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
101 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
102 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
103 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
104 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
105 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
106 ('ShaderNodeRGB', 'RGB', 'RGB'),
107 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
108 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
109 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
110 ('ShaderNodeValue', 'VALUE', 'Value'),
111 ('ShaderNodeVertexColor', 'VERTEX_COLOR', 'Vertex Color'),
112 ('ShaderNodeVolumeInfo', 'VOLUME_INFO', 'Volume Info'),
113 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
116 # (rna_type.identifier, type, rna_type.name)
117 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
118 # Keeping things in alphabetical orde so we don't need to sort later.
119 shaders_output_nodes_props = (
120 ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'),
121 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
122 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
123 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
125 # (rna_type.identifier, type, rna_type.name)
126 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
127 # Keeping things in alphabetical orde so we don't need to sort later.
128 shaders_shader_nodes_props = (
129 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
130 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
131 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
132 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
133 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
134 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
135 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
136 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
137 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
138 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
139 ('ShaderNodeBsdfHairPrincipled', 'BSDF_HAIR_PRINCIPLED', 'Principled Hair BSDF'),
140 ('ShaderNodeVolumePrincipled', 'PRINCIPLED_VOLUME', 'Principled Volume'),
141 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
142 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
143 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
144 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
145 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
146 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
147 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
148 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
149 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
151 # (rna_type.identifier, type, rna_type.name)
152 # Keeping things in alphabetical orde so we don't need to sort later.
153 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
154 shaders_texture_nodes_props = (
155 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
156 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
157 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
158 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
159 ('ShaderNodeTexIES', 'TEX_IES', 'IES Texture'),
160 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
161 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
162 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
163 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
164 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
165 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
166 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
167 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
168 ('ShaderNodeTexWhiteNoise', 'TEX_WHITE_NOISE', 'White Noise'),
170 # (rna_type.identifier, type, rna_type.name)
171 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
172 # Keeping things in alphabetical orde so we don't need to sort later.
173 shaders_color_nodes_props = (
174 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
175 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
176 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
177 ('ShaderNodeInvert', 'INVERT', 'Invert'),
178 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
179 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
180 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
182 # (rna_type.identifier, type, rna_type.name)
183 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
184 # Keeping things in alphabetical orde so we don't need to sort later.
185 shaders_vector_nodes_props = (
186 ('ShaderNodeBump', 'BUMP', 'Bump'),
187 ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'),
188 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
189 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
190 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
191 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
192 ('ShaderNodeVectorDisplacement', 'VECTOR_DISPLACEMENT', 'Vector Displacement'),
193 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
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 # Keeping things in alphabetical orde so we don't need to sort later.
198 shaders_converter_nodes_props = (
199 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
200 ('ShaderNodeClamp', 'CLAMP', 'Clamp'),
201 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
202 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
203 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
204 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
205 ('ShaderNodeMapRange', 'MAP_RANGE', 'Map Range'),
206 ('ShaderNodeMath', 'MATH', 'Math'),
207 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
208 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
209 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
210 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
211 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
212 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
214 # (rna_type.identifier, type, rna_type.name)
215 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
216 # Keeping things in alphabetical orde so we don't need to sort later.
217 shaders_layout_nodes_props = (
218 ('NodeFrame', 'FRAME', 'Frame'),
219 ('NodeReroute', 'REROUTE', 'Reroute'),
222 # compositing nodes
223 # (rna_type.identifier, type, rna_type.name)
224 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
225 # Keeping things in alphabetical orde so we don't need to sort later.
226 compo_input_nodes_props = (
227 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
228 ('CompositorNodeImage', 'IMAGE', 'Image'),
229 ('CompositorNodeMask', 'MASK', 'Mask'),
230 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
231 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
232 ('CompositorNodeRGB', 'RGB', 'RGB'),
233 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
234 ('CompositorNodeTime', 'TIME', 'Time'),
235 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
236 ('CompositorNodeValue', 'VALUE', 'Value'),
238 # (rna_type.identifier, type, rna_type.name)
239 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
240 # Keeping things in alphabetical orde so we don't need to sort later.
241 compo_output_nodes_props = (
242 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
243 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
244 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
245 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
246 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
248 # (rna_type.identifier, type, rna_type.name)
249 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
250 # Keeping things in alphabetical orde so we don't need to sort later.
251 compo_color_nodes_props = (
252 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
253 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
254 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
255 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
256 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
257 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
258 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
259 ('CompositorNodeInvert', 'INVERT', 'Invert'),
260 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
261 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
262 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
263 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
265 # (rna_type.identifier, type, rna_type.name)
266 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
267 # Keeping things in alphabetical orde so we don't need to sort later.
268 compo_converter_nodes_props = (
269 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
270 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
271 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
272 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
273 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
274 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
275 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
276 ('CompositorNodeMath', 'MATH', 'Math'),
277 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
278 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
279 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
280 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
281 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
282 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
283 ('CompositorNodeSwitchView', 'VIEWSWITCH', 'View Switch'),
285 # (rna_type.identifier, type, rna_type.name)
286 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
287 # Keeping things in alphabetical orde so we don't need to sort later.
288 compo_filter_nodes_props = (
289 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
290 ('CompositorNodeBlur', 'BLUR', 'Blur'),
291 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
292 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
293 ('CompositorNodeDenoise', 'DENOISE', 'Denoise'),
294 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
295 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
296 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
297 ('CompositorNodeFilter', 'FILTER', 'Filter'),
298 ('CompositorNodeGlare', 'GLARE', 'Glare'),
299 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
300 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
301 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
302 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
304 # (rna_type.identifier, type, rna_type.name)
305 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
306 # Keeping things in alphabetical orde so we don't need to sort later.
307 compo_vector_nodes_props = (
308 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
309 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
310 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
311 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
312 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
314 # (rna_type.identifier, type, rna_type.name)
315 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
316 # Keeping things in alphabetical orde so we don't need to sort later.
317 compo_matte_nodes_props = (
318 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
319 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
320 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
321 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
322 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
323 ('CompositorNodeCryptomatte', 'CRYPTOMATTE', 'Cryptomatte'),
324 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
325 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
326 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
327 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
328 ('CompositorNodeKeying', 'KEYING', 'Keying'),
329 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
330 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
332 # (rna_type.identifier, type, rna_type.name)
333 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
334 # Keeping things in alphabetical orde so we don't need to sort later.
335 compo_distort_nodes_props = (
336 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
337 ('CompositorNodeCrop', 'CROP', 'Crop'),
338 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
339 ('CompositorNodeFlip', 'FLIP', 'Flip'),
340 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
341 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
342 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
343 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
344 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
345 ('CompositorNodeScale', 'SCALE', 'Scale'),
346 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
347 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
348 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
350 # (rna_type.identifier, type, rna_type.name)
351 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
352 # Keeping things in alphabetical orde so we don't need to sort later.
353 compo_layout_nodes_props = (
354 ('NodeFrame', 'FRAME', 'Frame'),
355 ('NodeReroute', 'REROUTE', 'Reroute'),
356 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
358 # Blender Render material nodes
359 # (rna_type.identifier, type, rna_type.name)
360 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
361 blender_mat_input_nodes_props = (
362 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
363 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
364 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
365 ('ShaderNodeValue', 'VALUE', 'Value'),
366 ('ShaderNodeRGB', 'RGB', 'RGB'),
367 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
368 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
369 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
372 # (rna_type.identifier, type, rna_type.name)
373 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
374 blender_mat_output_nodes_props = (
375 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
378 # (rna_type.identifier, type, rna_type.name)
379 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
380 blender_mat_color_nodes_props = (
381 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
382 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
383 ('ShaderNodeInvert', 'INVERT', 'Invert'),
384 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
387 # (rna_type.identifier, type, rna_type.name)
388 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
389 blender_mat_vector_nodes_props = (
390 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
391 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
392 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
395 # (rna_type.identifier, type, rna_type.name)
396 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
397 blender_mat_converter_nodes_props = (
398 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
399 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
400 ('ShaderNodeMath', 'MATH', 'Math'),
401 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
402 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
403 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
404 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
405 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
406 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
409 # (rna_type.identifier, type, rna_type.name)
410 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
411 blender_mat_layout_nodes_props = (
412 ('NodeReroute', 'REROUTE', 'Reroute'),
415 # Texture Nodes
416 # (rna_type.identifier, type, rna_type.name)
417 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
418 texture_input_nodes_props = (
419 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
420 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
421 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
422 ('TextureNodeImage', 'IMAGE', 'Image'),
425 # (rna_type.identifier, type, rna_type.name)
426 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
427 texture_output_nodes_props = (
428 ('TextureNodeOutput', 'OUTPUT', 'Output'),
429 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
432 # (rna_type.identifier, type, rna_type.name)
433 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
434 texture_color_nodes_props = (
435 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
436 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
437 ('TextureNodeInvert', 'INVERT', 'Invert'),
438 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
439 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
440 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
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_pattern_nodes_props = (
446 ('TextureNodeChecker', 'CHECKER', 'Checker'),
447 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
450 # (rna_type.identifier, type, rna_type.name)
451 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
452 texture_textures_nodes_props = (
453 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
454 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
455 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
456 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
457 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
458 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
459 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
460 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
461 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
462 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
465 # (rna_type.identifier, type, rna_type.name)
466 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
467 texture_converter_nodes_props = (
468 ('TextureNodeMath', 'MATH', 'Math'),
469 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
470 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
471 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
472 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
475 # (rna_type.identifier, type, rna_type.name)
476 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
477 texture_distort_nodes_props = (
478 ('TextureNodeScale', 'SCALE', 'Scale'),
479 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
480 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
481 ('TextureNodeAt', 'AT', 'At'),
484 # (rna_type.identifier, type, rna_type.name)
485 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
486 texture_layout_nodes_props = (
487 ('NodeReroute', 'REROUTE', 'Reroute'),
490 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
491 # used list, not tuple for easy merging with other lists.
492 blend_types = [
493 ('MIX', 'Mix', 'Mix Mode'),
494 ('ADD', 'Add', 'Add Mode'),
495 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
496 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
497 ('SCREEN', 'Screen', 'Screen Mode'),
498 ('DIVIDE', 'Divide', 'Divide Mode'),
499 ('DIFFERENCE', 'Difference', 'Difference Mode'),
500 ('DARKEN', 'Darken', 'Darken Mode'),
501 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
502 ('OVERLAY', 'Overlay', 'Overlay Mode'),
503 ('DODGE', 'Dodge', 'Dodge Mode'),
504 ('BURN', 'Burn', 'Burn Mode'),
505 ('HUE', 'Hue', 'Hue Mode'),
506 ('SATURATION', 'Saturation', 'Saturation Mode'),
507 ('VALUE', 'Value', 'Value Mode'),
508 ('COLOR', 'Color', 'Color Mode'),
509 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
510 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
513 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
514 # used list, not tuple for easy merging with other lists.
515 operations = [
516 ('ADD', 'Add', 'Add Mode'),
517 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
518 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
519 ('DIVIDE', 'Divide', 'Divide Mode'),
520 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
521 ('SINE', 'Sine', 'Sine Mode'),
522 ('COSINE', 'Cosine', 'Cosine Mode'),
523 ('TANGENT', 'Tangent', 'Tangent Mode'),
524 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
525 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
526 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
527 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
528 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
529 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
530 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
531 ('POWER', 'Power', 'Power Mode'),
532 ('LOGARITHM', 'Logatithm', 'Logarithm Mode'),
533 ('SQRT', 'Square Root', 'Square Root Mode'),
534 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
535 ('EXPONENT', 'Exponent', 'Exponent Mode'),
536 ('MINIMUM', 'Minimum', 'Minimum Mode'),
537 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
538 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
539 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
540 ('SIGN', 'Sign', 'Sign Mode'),
541 ('COMPARE', 'Compare', 'Compare Mode'),
542 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
543 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
544 ('FRACT', 'Fraction', 'Fraction Mode'),
545 ('MODULO', 'Modulo', 'Modulo Mode'),
546 ('SNAP', 'Snap', 'Snap Mode'),
547 ('WRAP', 'Wrap', 'Wrap Mode'),
548 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
549 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
550 ('ROUND', 'Round', 'Round Mode'),
551 ('FLOOR', 'Floor', 'Floor Mode'),
552 ('CEIL', 'Ceil', 'Ceil Mode'),
553 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
554 ('RADIANS', 'To Radians', 'To Radians Mode'),
555 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
558 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
559 # used list, not tuple for easy merging with other lists.
560 navs = [
561 ('CURRENT', 'Current', 'Leave at current state'),
562 ('NEXT', 'Next', 'Next blend type/operation'),
563 ('PREV', 'Prev', 'Previous blend type/operation'),
566 draw_color_sets = {
567 "red_white": (
568 (1.0, 1.0, 1.0, 0.7),
569 (1.0, 0.0, 0.0, 0.7),
570 (0.8, 0.2, 0.2, 1.0)
572 "green": (
573 (0.0, 0.0, 0.0, 1.0),
574 (0.38, 0.77, 0.38, 1.0),
575 (0.38, 0.77, 0.38, 1.0)
577 "yellow": (
578 (0.0, 0.0, 0.0, 1.0),
579 (0.77, 0.77, 0.16, 1.0),
580 (0.77, 0.77, 0.16, 1.0)
582 "purple": (
583 (0.0, 0.0, 0.0, 1.0),
584 (0.38, 0.38, 0.77, 1.0),
585 (0.38, 0.38, 0.77, 1.0)
587 "grey": (
588 (0.0, 0.0, 0.0, 1.0),
589 (0.63, 0.63, 0.63, 1.0),
590 (0.63, 0.63, 0.63, 1.0)
592 "black": (
593 (1.0, 1.0, 1.0, 0.7),
594 (0.0, 0.0, 0.0, 0.7),
595 (0.2, 0.2, 0.2, 1.0)
599 viewer_socket_name = "tmp_viewer"
601 def is_visible_socket(socket):
602 return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
604 def nice_hotkey_name(punc):
605 # convert the ugly string name into the actual character
606 pairs = (
607 ('LEFTMOUSE', "LMB"),
608 ('MIDDLEMOUSE', "MMB"),
609 ('RIGHTMOUSE', "RMB"),
610 ('WHEELUPMOUSE', "Wheel Up"),
611 ('WHEELDOWNMOUSE', "Wheel Down"),
612 ('WHEELINMOUSE', "Wheel In"),
613 ('WHEELOUTMOUSE', "Wheel Out"),
614 ('ZERO', "0"),
615 ('ONE', "1"),
616 ('TWO', "2"),
617 ('THREE', "3"),
618 ('FOUR', "4"),
619 ('FIVE', "5"),
620 ('SIX', "6"),
621 ('SEVEN', "7"),
622 ('EIGHT', "8"),
623 ('NINE', "9"),
624 ('OSKEY', "Super"),
625 ('RET', "Enter"),
626 ('LINE_FEED', "Enter"),
627 ('SEMI_COLON', ";"),
628 ('PERIOD', "."),
629 ('COMMA', ","),
630 ('QUOTE', '"'),
631 ('MINUS', "-"),
632 ('SLASH', "/"),
633 ('BACK_SLASH', "\\"),
634 ('EQUAL', "="),
635 ('NUMPAD_1', "Numpad 1"),
636 ('NUMPAD_2', "Numpad 2"),
637 ('NUMPAD_3', "Numpad 3"),
638 ('NUMPAD_4', "Numpad 4"),
639 ('NUMPAD_5', "Numpad 5"),
640 ('NUMPAD_6', "Numpad 6"),
641 ('NUMPAD_7', "Numpad 7"),
642 ('NUMPAD_8', "Numpad 8"),
643 ('NUMPAD_9', "Numpad 9"),
644 ('NUMPAD_0', "Numpad 0"),
645 ('NUMPAD_PERIOD', "Numpad ."),
646 ('NUMPAD_SLASH', "Numpad /"),
647 ('NUMPAD_ASTERIX', "Numpad *"),
648 ('NUMPAD_MINUS', "Numpad -"),
649 ('NUMPAD_ENTER', "Numpad Enter"),
650 ('NUMPAD_PLUS', "Numpad +"),
652 nice_punc = False
653 for (ugly, nice) in pairs:
654 if punc == ugly:
655 nice_punc = nice
656 break
657 if not nice_punc:
658 nice_punc = punc.replace("_", " ").title()
659 return nice_punc
662 def force_update(context):
663 context.space_data.node_tree.update_tag()
666 def dpifac():
667 prefs = bpy.context.preferences.system
668 return prefs.dpi * prefs.pixel_size / 72
671 def node_mid_pt(node, axis):
672 if axis == 'x':
673 d = node.location.x + (node.dimensions.x / 2)
674 elif axis == 'y':
675 d = node.location.y - (node.dimensions.y / 2)
676 else:
677 d = 0
678 return d
681 def autolink(node1, node2, links):
682 link_made = False
684 for outp in node1.outputs:
685 for inp in node2.inputs:
686 if not inp.is_linked and inp.name == outp.name:
687 link_made = True
688 links.new(outp, inp)
689 return True
691 for outp in node1.outputs:
692 for inp in node2.inputs:
693 if not inp.is_linked and inp.type == outp.type:
694 link_made = True
695 links.new(outp, inp)
696 return True
698 # force some connection even if the type doesn't match
699 for outp in node1.outputs:
700 for inp in node2.inputs:
701 if not inp.is_linked:
702 link_made = True
703 links.new(outp, inp)
704 return True
706 # even if no sockets are open, force one of matching type
707 for outp in node1.outputs:
708 for inp in node2.inputs:
709 if inp.type == outp.type:
710 link_made = True
711 links.new(outp, inp)
712 return True
714 # do something!
715 for outp in node1.outputs:
716 for inp in node2.inputs:
717 link_made = True
718 links.new(outp, inp)
719 return True
721 print("Could not make a link from " + node1.name + " to " + node2.name)
722 return link_made
725 def node_at_pos(nodes, context, event):
726 nodes_near_mouse = []
727 nodes_under_mouse = []
728 target_node = None
730 store_mouse_cursor(context, event)
731 x, y = context.space_data.cursor_location
732 x = x
733 y = y
735 # Make a list of each corner (and middle of border) for each node.
736 # Will be sorted to find nearest point and thus nearest node
737 node_points_with_dist = []
738 for node in nodes:
739 skipnode = False
740 if node.type != 'FRAME': # no point trying to link to a frame node
741 locx = node.location.x
742 locy = node.location.y
743 dimx = node.dimensions.x/dpifac()
744 dimy = node.dimensions.y/dpifac()
745 if node.parent:
746 locx += node.parent.location.x
747 locy += node.parent.location.y
748 if node.parent.parent:
749 locx += node.parent.parent.location.x
750 locy += node.parent.parent.location.y
751 if node.parent.parent.parent:
752 locx += node.parent.parent.parent.location.x
753 locy += node.parent.parent.parent.location.y
754 if node.parent.parent.parent.parent:
755 # Support three levels or parenting
756 # There's got to be a better way to do this...
757 skipnode = True
758 if not skipnode:
759 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
760 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
761 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
762 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
764 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
765 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
766 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
767 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
769 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
771 for node in nodes:
772 if node.type != 'FRAME' and skipnode == False:
773 locx = node.location.x
774 locy = node.location.y
775 dimx = node.dimensions.x/dpifac()
776 dimy = node.dimensions.y/dpifac()
777 if node.parent:
778 locx += node.parent.location.x
779 locy += node.parent.location.y
780 if (locx <= x <= locx + dimx) and \
781 (locy - dimy <= y <= locy):
782 nodes_under_mouse.append(node)
784 if len(nodes_under_mouse) == 1:
785 if nodes_under_mouse[0] != nearest_node:
786 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
787 else:
788 target_node = nearest_node # else use the nearest node
789 else:
790 target_node = nearest_node
791 return target_node
794 def store_mouse_cursor(context, event):
795 space = context.space_data
796 v2d = context.region.view2d
797 tree = space.edit_tree
799 # convert mouse position to the View2D for later node placement
800 if context.region.type == 'WINDOW':
801 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
802 else:
803 space.cursor_location = tree.view_center
805 def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
806 shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
808 vertices = ((x1, y1), (x2, y2))
809 vertex_colors = ((colour[0]+(1.0-colour[0])/4,
810 colour[1]+(1.0-colour[1])/4,
811 colour[2]+(1.0-colour[2])/4,
812 colour[3]+(1.0-colour[3])/4),
813 colour)
815 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
816 bgl.glLineWidth(size * dpifac())
818 shader.bind()
819 batch.draw(shader)
822 def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
823 radius = radius * dpifac()
824 sides = 12
825 vertices = [(radius * cos(i * 2 * pi / sides) + mx,
826 radius * sin(i * 2 * pi / sides) + my)
827 for i in range(sides + 1)]
829 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
830 shader.bind()
831 shader.uniform_float("color", colour)
832 batch.draw(shader)
834 def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
835 area_width = bpy.context.area.width - (16*dpifac()) - 1
836 bottom_bar = (16*dpifac()) + 1
837 sides = 16
838 radius = radius*dpifac()
840 nlocx = (node.location.x+1)*dpifac()
841 nlocy = (node.location.y+1)*dpifac()
842 ndimx = node.dimensions.x
843 ndimy = node.dimensions.y
844 # This is a stupid way to do this... TODO use while loop
845 if node.parent:
846 nlocx += node.parent.location.x
847 nlocy += node.parent.location.y
848 if node.parent.parent:
849 nlocx += node.parent.parent.location.x
850 nlocy += node.parent.parent.location.y
851 if node.parent.parent.parent:
852 nlocx += node.parent.parent.parent.location.x
853 nlocy += node.parent.parent.parent.location.y
855 if node.hide:
856 nlocx += -1
857 nlocy += 5
858 if node.type == 'REROUTE':
859 #nlocx += 1
860 nlocy -= 1
861 ndimx = 0
862 ndimy = 0
863 radius += 6
865 # Top left corner
866 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
867 vertices = [(mx,my)]
868 for i in range(sides+1):
869 if (4<=i<=8):
870 if my > bottom_bar and mx < area_width:
871 cosine = radius * cos(i * 2 * pi / sides) + mx
872 sine = radius * sin(i * 2 * pi / sides) + my
873 vertices.append((cosine,sine))
874 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
875 shader.bind()
876 shader.uniform_float("color", colour)
877 batch.draw(shader)
879 # Top right corner
880 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
881 vertices = [(mx,my)]
882 for i in range(sides+1):
883 if (0<=i<=4):
884 if my > bottom_bar and mx < area_width:
885 cosine = radius * cos(i * 2 * pi / sides) + mx
886 sine = radius * sin(i * 2 * pi / sides) + my
887 vertices.append((cosine,sine))
888 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
889 shader.bind()
890 shader.uniform_float("color", colour)
891 batch.draw(shader)
893 # Bottom left corner
894 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
895 vertices = [(mx,my)]
896 for i in range(sides+1):
897 if (8<=i<=12):
898 if my > bottom_bar and mx < area_width:
899 cosine = radius * cos(i * 2 * pi / sides) + mx
900 sine = radius * sin(i * 2 * pi / sides) + my
901 vertices.append((cosine,sine))
902 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
903 shader.bind()
904 shader.uniform_float("color", colour)
905 batch.draw(shader)
907 # Bottom right corner
908 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
909 vertices = [(mx,my)]
910 for i in range(sides+1):
911 if (12<=i<=16):
912 if my > bottom_bar and mx < area_width:
913 cosine = radius * cos(i * 2 * pi / sides) + mx
914 sine = radius * sin(i * 2 * pi / sides) + my
915 vertices.append((cosine,sine))
916 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
917 shader.bind()
918 shader.uniform_float("color", colour)
919 batch.draw(shader)
921 # prepare drawing all edges in one batch
922 vertices = []
923 indices = []
924 id_last = 0
926 # Left edge
927 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
928 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
929 if m1x < area_width and m2x < area_width:
930 vertices.extend([(m2x-radius,m2y), (m2x,m2y),
931 (m1x,m1y), (m1x-radius,m1y)])
932 indices.extend([(id_last, id_last+1, id_last+3),
933 (id_last+3, id_last+1, id_last+2)])
934 id_last += 4
936 # Top edge
937 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
938 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
939 m1x = min(m1x, area_width)
940 m2x = min(m2x, area_width)
941 if m1y > bottom_bar and m2y > bottom_bar:
942 vertices.extend([(m1x,m1y), (m2x,m1y),
943 (m2x,m1y+radius), (m1x,m1y+radius)])
944 indices.extend([(id_last, id_last+1, id_last+3),
945 (id_last+3, id_last+1, id_last+2)])
946 id_last += 4
948 # Right edge
949 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
950 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
951 m1y = max(m1y, bottom_bar)
952 m2y = max(m2y, bottom_bar)
953 if m1x < area_width and m2x < area_width:
954 vertices.extend([(m1x,m2y), (m1x+radius,m2y),
955 (m1x+radius,m1y), (m1x,m1y)])
956 indices.extend([(id_last, id_last+1, id_last+3),
957 (id_last+3, id_last+1, id_last+2)])
958 id_last += 4
960 # Bottom edge
961 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
962 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
963 m1x = min(m1x, area_width)
964 m2x = min(m2x, area_width)
965 if m1y > bottom_bar and m2y > bottom_bar:
966 vertices.extend([(m1x,m2y), (m2x,m2y),
967 (m2x,m1y-radius), (m1x,m1y-radius)])
968 indices.extend([(id_last, id_last+1, id_last+3),
969 (id_last+3, id_last+1, id_last+2)])
971 # now draw all edges in one batch
972 if len(vertices) != 0:
973 batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
974 shader.bind()
975 shader.uniform_float("color", colour)
976 batch.draw(shader)
978 def draw_callback_nodeoutline(self, context, mode):
979 if self.mouse_path:
981 bgl.glLineWidth(1)
982 bgl.glEnable(bgl.GL_BLEND)
983 bgl.glEnable(bgl.GL_LINE_SMOOTH)
984 bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
986 nodes, links = get_nodes_links(context)
988 shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
990 if mode == "LINK":
991 col_outer = (1.0, 0.2, 0.2, 0.4)
992 col_inner = (0.0, 0.0, 0.0, 0.5)
993 col_circle_inner = (0.3, 0.05, 0.05, 1.0)
994 elif mode == "LINKMENU":
995 col_outer = (0.4, 0.6, 1.0, 0.4)
996 col_inner = (0.0, 0.0, 0.0, 0.5)
997 col_circle_inner = (0.08, 0.15, .3, 1.0)
998 elif mode == "MIX":
999 col_outer = (0.2, 1.0, 0.2, 0.4)
1000 col_inner = (0.0, 0.0, 0.0, 0.5)
1001 col_circle_inner = (0.05, 0.3, 0.05, 1.0)
1003 m1x = self.mouse_path[0][0]
1004 m1y = self.mouse_path[0][1]
1005 m2x = self.mouse_path[-1][0]
1006 m2y = self.mouse_path[-1][1]
1008 n1 = nodes[context.scene.NWLazySource]
1009 n2 = nodes[context.scene.NWLazyTarget]
1011 if n1 == n2:
1012 col_outer = (0.4, 0.4, 0.4, 0.4)
1013 col_inner = (0.0, 0.0, 0.0, 0.5)
1014 col_circle_inner = (0.2, 0.2, 0.2, 1.0)
1016 draw_rounded_node_border(shader, n1, radius=6, colour=col_outer) # outline
1017 draw_rounded_node_border(shader, n1, radius=5, colour=col_inner) # inner
1018 draw_rounded_node_border(shader, n2, radius=6, colour=col_outer) # outline
1019 draw_rounded_node_border(shader, n2, radius=5, colour=col_inner) # inner
1021 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
1022 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
1024 # circle outline
1025 draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
1026 draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
1028 # circle inner
1029 draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
1030 draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
1032 bgl.glDisable(bgl.GL_BLEND)
1033 bgl.glDisable(bgl.GL_LINE_SMOOTH)
1034 def get_active_tree(context):
1035 tree = context.space_data.node_tree
1036 path = []
1037 # Get nodes from currently edited tree.
1038 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1039 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1040 # the same as context.active_node, the user is in a group.
1041 # Check recursively until we find the real active node_tree:
1042 if tree.nodes.active:
1043 while tree.nodes.active != context.active_node:
1044 tree = tree.nodes.active.node_tree
1045 path.append(tree)
1046 return tree, path
1048 def get_nodes_links(context):
1049 tree, path = get_active_tree(context)
1050 return tree.nodes, tree.links
1052 def is_viewer_socket(socket):
1053 # checks if a internal socket is a valid viewer socket
1054 return socket.name == viewer_socket_name and socket.NWViewerSocket
1056 def get_internal_socket(socket):
1057 #get the internal socket from a socket inside or outside the group
1058 node = socket.node
1059 if node.type == 'GROUP_OUTPUT':
1060 source_iterator = node.inputs
1061 iterator = node.id_data.outputs
1062 elif node.type == 'GROUP_INPUT':
1063 source_iterator = node.outputs
1064 iterator = node.id_data.inputs
1065 elif hasattr(node, "node_tree"):
1066 if socket.is_output:
1067 source_iterator = node.outputs
1068 iterator = node.node_tree.outputs
1069 else:
1070 source_iterator = node.inputs
1071 iterator = node.node_tree.inputs
1072 else:
1073 return None
1075 for i, s in enumerate(source_iterator):
1076 if s == socket:
1077 break
1078 return iterator[i]
1080 def is_viewer_link(link, output_node):
1081 if "Emission Viewer" in link.to_node.name or link.to_node == output_node and link.to_socket == output_node.inputs[0]:
1082 return True
1083 if link.to_node.type == 'GROUP_OUTPUT':
1084 socket = get_internal_socket(link.to_socket)
1085 if is_viewer_socket(socket):
1086 return True
1087 return False
1089 def get_group_output_node(tree):
1090 for node in tree.nodes:
1091 if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
1092 return node
1094 def get_output_location(tree):
1095 # get right-most location
1096 sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
1097 max_xloc_node = sorted_by_xloc[-1]
1098 if max_xloc_node.name == 'Emission Viewer':
1099 max_xloc_node = sorted_by_xloc[-2]
1101 # get average y location
1102 sum_yloc = 0
1103 for node in tree.nodes:
1104 sum_yloc += node.location.y
1106 loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1107 loc_y = sum_yloc / len(tree.nodes)
1108 return loc_x, loc_y
1110 # Principled prefs
1111 class NWPrincipledPreferences(bpy.types.PropertyGroup):
1112 base_color: StringProperty(
1113 name='Base Color',
1114 default='diffuse diff albedo base col color',
1115 description='Naming Components for Base Color maps')
1116 sss_color: StringProperty(
1117 name='Subsurface Color',
1118 default='sss subsurface',
1119 description='Naming Components for Subsurface Color maps')
1120 metallic: StringProperty(
1121 name='Metallic',
1122 default='metallic metalness metal mtl',
1123 description='Naming Components for metallness maps')
1124 specular: StringProperty(
1125 name='Specular',
1126 default='specularity specular spec spc',
1127 description='Naming Components for Specular maps')
1128 normal: StringProperty(
1129 name='Normal',
1130 default='normal nor nrm nrml norm',
1131 description='Naming Components for Normal maps')
1132 bump: StringProperty(
1133 name='Bump',
1134 default='bump bmp',
1135 description='Naming Components for bump maps')
1136 rough: StringProperty(
1137 name='Roughness',
1138 default='roughness rough rgh',
1139 description='Naming Components for roughness maps')
1140 gloss: StringProperty(
1141 name='Gloss',
1142 default='gloss glossy glossiness',
1143 description='Naming Components for glossy maps')
1144 displacement: StringProperty(
1145 name='Displacement',
1146 default='displacement displace disp dsp height heightmap',
1147 description='Naming Components for displacement maps')
1149 # Addon prefs
1150 class NWNodeWrangler(bpy.types.AddonPreferences):
1151 bl_idname = __name__
1153 merge_hide: EnumProperty(
1154 name="Hide Mix nodes",
1155 items=(
1156 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1157 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1158 ("NEVER", "Never", "Never collapse the new merge nodes")
1160 default='NON_SHADER',
1161 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1162 merge_position: EnumProperty(
1163 name="Mix Node Position",
1164 items=(
1165 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1166 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1168 default='CENTER',
1169 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1171 show_hotkey_list: BoolProperty(
1172 name="Show Hotkey List",
1173 default=False,
1174 description="Expand this box into a list of all the hotkeys for functions in this addon"
1176 hotkey_list_filter: StringProperty(
1177 name=" Filter by Name",
1178 default="",
1179 description="Show only hotkeys that have this text in their name"
1181 show_principled_lists: BoolProperty(
1182 name="Show Principled naming tags",
1183 default=False,
1184 description="Expand this box into a list of all naming tags for principled texture setup"
1186 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
1188 def draw(self, context):
1189 layout = self.layout
1190 col = layout.column()
1191 col.prop(self, "merge_position")
1192 col.prop(self, "merge_hide")
1194 box = layout.box()
1195 col = box.column(align=True)
1196 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
1197 if self.show_principled_lists:
1198 tags = self.principled_tags
1200 col.prop(tags, "base_color")
1201 col.prop(tags, "sss_color")
1202 col.prop(tags, "metallic")
1203 col.prop(tags, "specular")
1204 col.prop(tags, "rough")
1205 col.prop(tags, "gloss")
1206 col.prop(tags, "normal")
1207 col.prop(tags, "bump")
1208 col.prop(tags, "displacement")
1210 box = layout.box()
1211 col = box.column(align=True)
1212 hotkey_button_name = "Show Hotkey List"
1213 if self.show_hotkey_list:
1214 hotkey_button_name = "Hide Hotkey List"
1215 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
1216 if self.show_hotkey_list:
1217 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
1218 col.separator()
1219 for hotkey in kmi_defs:
1220 if hotkey[7]:
1221 hotkey_name = hotkey[7]
1223 if self.hotkey_list_filter.lower() in hotkey_name.lower():
1224 row = col.row(align=True)
1225 row.label(text=hotkey_name)
1226 keystr = nice_hotkey_name(hotkey[1])
1227 if hotkey[4]:
1228 keystr = "Shift " + keystr
1229 if hotkey[5]:
1230 keystr = "Alt " + keystr
1231 if hotkey[3]:
1232 keystr = "Ctrl " + keystr
1233 row.label(text=keystr)
1237 def nw_check(context):
1238 space = context.space_data
1239 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree"]
1241 valid = False
1242 if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
1243 valid = True
1245 return valid
1247 class NWBase:
1248 @classmethod
1249 def poll(cls, context):
1250 return nw_check(context)
1253 # OPERATORS
1254 class NWLazyMix(Operator, NWBase):
1255 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1256 bl_idname = "node.nw_lazy_mix"
1257 bl_label = "Mix Nodes"
1258 bl_options = {'REGISTER', 'UNDO'}
1260 def modal(self, context, event):
1261 context.area.tag_redraw()
1262 nodes, links = get_nodes_links(context)
1263 cont = True
1265 start_pos = [event.mouse_region_x, event.mouse_region_y]
1267 node1 = None
1268 if not context.scene.NWBusyDrawing:
1269 node1 = node_at_pos(nodes, context, event)
1270 if node1:
1271 context.scene.NWBusyDrawing = node1.name
1272 else:
1273 if context.scene.NWBusyDrawing != 'STOP':
1274 node1 = nodes[context.scene.NWBusyDrawing]
1276 context.scene.NWLazySource = node1.name
1277 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1279 if event.type == 'MOUSEMOVE':
1280 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1282 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1283 end_pos = [event.mouse_region_x, event.mouse_region_y]
1284 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1286 node2 = None
1287 node2 = node_at_pos(nodes, context, event)
1288 if node2:
1289 context.scene.NWBusyDrawing = node2.name
1291 if node1 == node2:
1292 cont = False
1294 if cont:
1295 if node1 and node2:
1296 for node in nodes:
1297 node.select = False
1298 node1.select = True
1299 node2.select = True
1301 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
1303 context.scene.NWBusyDrawing = ""
1304 return {'FINISHED'}
1306 elif event.type == 'ESC':
1307 print('cancelled')
1308 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1309 return {'CANCELLED'}
1311 return {'RUNNING_MODAL'}
1313 def invoke(self, context, event):
1314 if context.area.type == 'NODE_EDITOR':
1315 # the arguments we pass the the callback
1316 args = (self, context, 'MIX')
1317 # Add the region OpenGL drawing callback
1318 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1319 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1321 self.mouse_path = []
1323 context.window_manager.modal_handler_add(self)
1324 return {'RUNNING_MODAL'}
1325 else:
1326 self.report({'WARNING'}, "View3D not found, cannot run operator")
1327 return {'CANCELLED'}
1330 class NWLazyConnect(Operator, NWBase):
1331 """Connect two nodes without clicking a specific socket (automatically determined"""
1332 bl_idname = "node.nw_lazy_connect"
1333 bl_label = "Lazy Connect"
1334 bl_options = {'REGISTER', 'UNDO'}
1335 with_menu: BoolProperty()
1337 def modal(self, context, event):
1338 context.area.tag_redraw()
1339 nodes, links = get_nodes_links(context)
1340 cont = True
1342 start_pos = [event.mouse_region_x, event.mouse_region_y]
1344 node1 = None
1345 if not context.scene.NWBusyDrawing:
1346 node1 = node_at_pos(nodes, context, event)
1347 if node1:
1348 context.scene.NWBusyDrawing = node1.name
1349 else:
1350 if context.scene.NWBusyDrawing != 'STOP':
1351 node1 = nodes[context.scene.NWBusyDrawing]
1353 context.scene.NWLazySource = node1.name
1354 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1356 if event.type == 'MOUSEMOVE':
1357 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1359 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1360 end_pos = [event.mouse_region_x, event.mouse_region_y]
1361 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1363 node2 = None
1364 node2 = node_at_pos(nodes, context, event)
1365 if node2:
1366 context.scene.NWBusyDrawing = node2.name
1368 if node1 == node2:
1369 cont = False
1371 link_success = False
1372 if cont:
1373 if node1 and node2:
1374 original_sel = []
1375 original_unsel = []
1376 for node in nodes:
1377 if node.select == True:
1378 node.select = False
1379 original_sel.append(node)
1380 else:
1381 original_unsel.append(node)
1382 node1.select = True
1383 node2.select = True
1385 #link_success = autolink(node1, node2, links)
1386 if self.with_menu:
1387 if len(node1.outputs) > 1 and node2.inputs:
1388 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1389 elif len(node1.outputs) == 1:
1390 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1391 else:
1392 link_success = autolink(node1, node2, links)
1394 for node in original_sel:
1395 node.select = True
1396 for node in original_unsel:
1397 node.select = False
1399 if link_success:
1400 force_update(context)
1401 context.scene.NWBusyDrawing = ""
1402 return {'FINISHED'}
1404 elif event.type == 'ESC':
1405 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1406 return {'CANCELLED'}
1408 return {'RUNNING_MODAL'}
1410 def invoke(self, context, event):
1411 if context.area.type == 'NODE_EDITOR':
1412 nodes, links = get_nodes_links(context)
1413 node = node_at_pos(nodes, context, event)
1414 if node:
1415 context.scene.NWBusyDrawing = node.name
1417 # the arguments we pass the the callback
1418 mode = "LINK"
1419 if self.with_menu:
1420 mode = "LINKMENU"
1421 args = (self, context, mode)
1422 # Add the region OpenGL drawing callback
1423 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1424 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1426 self.mouse_path = []
1428 context.window_manager.modal_handler_add(self)
1429 return {'RUNNING_MODAL'}
1430 else:
1431 self.report({'WARNING'}, "View3D not found, cannot run operator")
1432 return {'CANCELLED'}
1435 class NWDeleteUnused(Operator, NWBase):
1436 """Delete all nodes whose output is not used"""
1437 bl_idname = 'node.nw_del_unused'
1438 bl_label = 'Delete Unused Nodes'
1439 bl_options = {'REGISTER', 'UNDO'}
1441 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1442 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1444 def is_unused_node(self, node):
1445 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1446 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1447 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1448 if node.type in end_types:
1449 return False
1451 for output in node.outputs:
1452 if output.links:
1453 return False
1454 return True
1456 @classmethod
1457 def poll(cls, context):
1458 valid = False
1459 if nw_check(context):
1460 if context.space_data.node_tree.nodes:
1461 valid = True
1462 return valid
1464 def execute(self, context):
1465 nodes, links = get_nodes_links(context)
1467 # Store selection
1468 selection = []
1469 for node in nodes:
1470 if node.select == True:
1471 selection.append(node.name)
1473 for node in nodes:
1474 node.select = False
1476 deleted_nodes = []
1477 temp_deleted_nodes = []
1478 del_unused_iterations = len(nodes)
1479 for it in range(0, del_unused_iterations):
1480 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1481 for node in nodes:
1482 if self.is_unused_node(node):
1483 node.select = True
1484 deleted_nodes.append(node.name)
1485 bpy.ops.node.delete()
1487 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1488 break
1490 if self.delete_frames:
1491 repeat = True
1492 while repeat:
1493 frames_in_use = []
1494 frames = []
1495 repeat = False
1496 for node in nodes:
1497 if node.parent:
1498 frames_in_use.append(node.parent)
1499 for node in nodes:
1500 if node.type == 'FRAME' and node not in frames_in_use:
1501 frames.append(node)
1502 if node.parent:
1503 repeat = True # repeat for nested frames
1504 for node in frames:
1505 if node not in frames_in_use:
1506 node.select = True
1507 deleted_nodes.append(node.name)
1508 bpy.ops.node.delete()
1510 if self.delete_muted:
1511 for node in nodes:
1512 if node.mute:
1513 node.select = True
1514 deleted_nodes.append(node.name)
1515 bpy.ops.node.delete_reconnect()
1517 # get unique list of deleted nodes (iterations would count the same node more than once)
1518 deleted_nodes = list(set(deleted_nodes))
1519 for n in deleted_nodes:
1520 self.report({'INFO'}, "Node " + n + " deleted")
1521 num_deleted = len(deleted_nodes)
1522 n = ' node'
1523 if num_deleted > 1:
1524 n += 's'
1525 if num_deleted:
1526 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1527 else:
1528 self.report({'INFO'}, "Nothing deleted")
1530 # Restore selection
1531 nodes, links = get_nodes_links(context)
1532 for node in nodes:
1533 if node.name in selection:
1534 node.select = True
1535 return {'FINISHED'}
1537 def invoke(self, context, event):
1538 return context.window_manager.invoke_confirm(self, event)
1541 class NWSwapLinks(Operator, NWBase):
1542 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1543 bl_idname = 'node.nw_swap_links'
1544 bl_label = 'Swap Links'
1545 bl_options = {'REGISTER', 'UNDO'}
1547 @classmethod
1548 def poll(cls, context):
1549 valid = False
1550 if nw_check(context):
1551 if context.selected_nodes:
1552 valid = len(context.selected_nodes) <= 2
1553 return valid
1555 def execute(self, context):
1556 nodes, links = get_nodes_links(context)
1557 selected_nodes = context.selected_nodes
1558 n1 = selected_nodes[0]
1560 # Swap outputs
1561 if len(selected_nodes) == 2:
1562 n2 = selected_nodes[1]
1563 if n1.outputs and n2.outputs:
1564 n1_outputs = []
1565 n2_outputs = []
1567 out_index = 0
1568 for output in n1.outputs:
1569 if output.links:
1570 for link in output.links:
1571 n1_outputs.append([out_index, link.to_socket])
1572 links.remove(link)
1573 out_index += 1
1575 out_index = 0
1576 for output in n2.outputs:
1577 if output.links:
1578 for link in output.links:
1579 n2_outputs.append([out_index, link.to_socket])
1580 links.remove(link)
1581 out_index += 1
1583 for connection in n1_outputs:
1584 try:
1585 links.new(n2.outputs[connection[0]], connection[1])
1586 except:
1587 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1588 for connection in n2_outputs:
1589 try:
1590 links.new(n1.outputs[connection[0]], connection[1])
1591 except:
1592 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1593 else:
1594 if n1.outputs or n2.outputs:
1595 self.report({'WARNING'}, "One of the nodes has no outputs!")
1596 else:
1597 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1599 # Swap Inputs
1600 elif len(selected_nodes) == 1:
1601 if n1.inputs:
1602 types = []
1604 for i1 in n1.inputs:
1605 if i1.is_linked:
1606 similar_types = 0
1607 for i2 in n1.inputs:
1608 if i1.type == i2.type and i2.is_linked:
1609 similar_types += 1
1610 types.append ([i1, similar_types, i])
1611 i += 1
1612 types.sort(key=lambda k: k[1], reverse=True)
1614 if types:
1615 t = types[0]
1616 if t[1] == 2:
1617 for i2 in n1.inputs:
1618 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1619 pair = [t[0], i2]
1620 i1f = pair[0].links[0].from_socket
1621 i1t = pair[0].links[0].to_socket
1622 i2f = pair[1].links[0].from_socket
1623 i2t = pair[1].links[0].to_socket
1624 links.new(i1f, i2t)
1625 links.new(i2f, i1t)
1626 if t[1] == 1:
1627 if len(types) == 1:
1628 fs = t[0].links[0].from_socket
1629 i = t[2]
1630 links.remove(t[0].links[0])
1631 if i+1 == len(n1.inputs):
1632 i = -1
1633 i += 1
1634 while n1.inputs[i].is_linked:
1635 i += 1
1636 links.new(fs, n1.inputs[i])
1637 elif len(types) == 2:
1638 i1f = types[0][0].links[0].from_socket
1639 i1t = types[0][0].links[0].to_socket
1640 i2f = types[1][0].links[0].from_socket
1641 i2t = types[1][0].links[0].to_socket
1642 links.new(i1f, i2t)
1643 links.new(i2f, i1t)
1645 else:
1646 self.report({'WARNING'}, "This node has no input connections to swap!")
1647 else:
1648 self.report({'WARNING'}, "This node has no inputs to swap!")
1650 force_update(context)
1651 return {'FINISHED'}
1654 class NWResetBG(Operator, NWBase):
1655 """Reset the zoom and position of the background image"""
1656 bl_idname = 'node.nw_bg_reset'
1657 bl_label = 'Reset Backdrop'
1658 bl_options = {'REGISTER', 'UNDO'}
1660 @classmethod
1661 def poll(cls, context):
1662 valid = False
1663 if nw_check(context):
1664 snode = context.space_data
1665 valid = snode.tree_type == 'CompositorNodeTree'
1666 return valid
1668 def execute(self, context):
1669 context.space_data.backdrop_zoom = 1
1670 context.space_data.backdrop_offset[0] = 0
1671 context.space_data.backdrop_offset[1] = 0
1672 return {'FINISHED'}
1675 class NWAddAttrNode(Operator, NWBase):
1676 """Add an Attribute node with this name"""
1677 bl_idname = 'node.nw_add_attr_node'
1678 bl_label = 'Add UV map'
1679 bl_options = {'REGISTER', 'UNDO'}
1681 attr_name: StringProperty()
1683 def execute(self, context):
1684 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1685 nodes, links = get_nodes_links(context)
1686 nodes.active.attribute_name = self.attr_name
1687 return {'FINISHED'}
1689 class NWEmissionViewer(Operator, NWBase):
1690 bl_idname = "node.nw_emission_viewer"
1691 bl_label = "Emission Viewer"
1692 bl_description = "Connect active node to Emission Shader for shadeless previews"
1693 bl_options = {'REGISTER', 'UNDO'}
1695 def __init__(self):
1696 self.shader_output_type = ""
1697 self.shader_output_ident = ""
1698 self.shader_viewer_ident = ""
1700 @classmethod
1701 def poll(cls, context):
1702 if nw_check(context):
1703 space = context.space_data
1704 if space.tree_type == 'ShaderNodeTree':
1705 if context.active_node:
1706 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1707 return True
1708 else:
1709 return True
1710 return False
1712 def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
1713 #check if a viewer output already exists in a node group otherwise create
1714 if hasattr(node, "node_tree"):
1715 index = None
1716 if len(node.node_tree.outputs):
1717 free_socket = None
1718 for i, socket in enumerate(node.node_tree.outputs):
1719 if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
1720 #if viewer output is already used but leads to the same socket we can still use it
1721 is_used = self.is_socket_used_other_mats(socket)
1722 if is_used:
1723 if connect_socket == None:
1724 continue
1725 groupout = get_group_output_node(node.node_tree)
1726 groupout_input = groupout.inputs[i]
1727 links = groupout_input.links
1728 if connect_socket not in [link.from_socket for link in links]:
1729 continue
1730 index=i
1731 break
1732 if not free_socket:
1733 free_socket = i
1734 if not index and free_socket:
1735 index = free_socket
1737 if not index:
1738 #create viewer socket
1739 node.node_tree.outputs.new(socket_type, viewer_socket_name)
1740 index = len(node.node_tree.outputs) - 1
1741 node.node_tree.outputs[index].NWViewerSocket = True
1742 return index
1744 def init_shader_variables(self, space, shader_type):
1745 if shader_type == 'OBJECT':
1746 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1747 self.shader_output_type = "OUTPUT_MATERIAL"
1748 self.shader_output_ident = "ShaderNodeOutputMaterial"
1749 self.shader_viewer_ident = "ShaderNodeEmission"
1750 else:
1751 self.shader_output_type = "OUTPUT_LIGHT"
1752 self.shader_output_ident = "ShaderNodeOutputLight"
1753 self.shader_viewer_ident = "ShaderNodeEmission"
1755 elif shader_type == 'WORLD':
1756 self.shader_output_type = "OUTPUT_WORLD"
1757 self.shader_output_ident = "ShaderNodeOutputWorld"
1758 self.shader_viewer_ident = "ShaderNodeBackground"
1760 def get_shader_output_node(self, tree):
1761 for node in tree.nodes:
1762 if node.type == self.shader_output_type and node.is_active_output == True:
1763 return node
1765 @classmethod
1766 def ensure_group_output(cls, tree):
1767 #check if a group output node exists otherwise create
1768 groupout = get_group_output_node(tree)
1769 if not groupout:
1770 groupout = tree.nodes.new('NodeGroupOutput')
1771 loc_x, loc_y = get_output_location(tree)
1772 groupout.location.x = loc_x
1773 groupout.location.y = loc_y
1774 groupout.select = False
1775 return groupout
1777 @classmethod
1778 def search_sockets(cls, node, sockets, index=None):
1779 #recursevley scan nodes for viewer sockets and store in list
1780 for i, input_socket in enumerate(node.inputs):
1781 if index and i != index:
1782 continue
1783 if len(input_socket.links):
1784 link = input_socket.links[0]
1785 next_node = link.from_node
1786 external_socket = link.from_socket
1787 if hasattr(next_node, "node_tree"):
1788 for socket_index, s in enumerate(next_node.outputs):
1789 if s == external_socket:
1790 break
1791 socket = next_node.node_tree.outputs[socket_index]
1792 if is_viewer_socket(socket) and socket not in sockets:
1793 sockets.append(socket)
1794 #continue search inside of node group but restrict socket to where we came from
1795 groupout = get_group_output_node(next_node.node_tree)
1796 cls.search_sockets(groupout, sockets, index=socket_index)
1798 @classmethod
1799 def scan_nodes(cls, tree, sockets):
1800 # get all viewer sockets in a material tree
1801 for node in tree.nodes:
1802 if hasattr(node, "node_tree"):
1803 for socket in node.node_tree.outputs:
1804 if is_viewer_socket(socket) and (socket not in sockets):
1805 sockets.append(socket)
1806 cls.scan_nodes(node.node_tree, sockets)
1808 def link_leads_to_used_socket(self, link):
1809 #return True if link leads to a socket that is already used in this material
1810 socket = get_internal_socket(link.to_socket)
1811 return (socket and self.is_socket_used_active_mat(socket))
1813 def is_socket_used_active_mat(self, socket):
1814 #ensure used sockets in active material is calculated and check given socket
1815 if not hasattr(self, "used_viewer_sockets_active_mat"):
1816 self.used_viewer_sockets_active_mat = []
1817 materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
1818 if materialout:
1819 emission = self.get_viewer_node(materialout)
1820 self.search_sockets((emission if emission else materialout), self.used_viewer_sockets_active_mat)
1821 return socket in self.used_viewer_sockets_active_mat
1823 def is_socket_used_other_mats(self, socket):
1824 #ensure used sockets in other materials are calculated and check given socket
1825 if not hasattr(self, "used_viewer_sockets_other_mats"):
1826 self.used_viewer_sockets_other_mats = []
1827 for mat in bpy.data.materials:
1828 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
1829 continue
1830 # get viewer node
1831 materialout = self.get_shader_output_node(mat.node_tree)
1832 if materialout:
1833 emission = self.get_viewer_node(materialout)
1834 self.search_sockets((emission if emission else materialout), self.used_viewer_sockets_other_mats)
1835 return socket in self.used_viewer_sockets_other_mats
1837 @staticmethod
1838 def get_viewer_node(materialout):
1839 input_socket = materialout.inputs[0]
1840 if len(input_socket.links) > 0:
1841 node = input_socket.links[0].from_node
1842 if node.type == 'EMISSION' and node.name == "Emission Viewer":
1843 return node
1845 def invoke(self, context, event):
1846 space = context.space_data
1847 shader_type = space.shader_type
1848 self.init_shader_variables(space, shader_type)
1849 shader_types = [x[1] for x in shaders_shader_nodes_props]
1850 mlocx = event.mouse_region_x
1851 mlocy = event.mouse_region_y
1852 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1853 if 'FINISHED' in select_node: # only run if mouse click is on a node
1854 active_tree, path_to_tree = get_active_tree(context)
1855 nodes, links = active_tree.nodes, active_tree.links
1856 base_node_tree = space.node_tree
1857 active = nodes.active
1858 output_types = [x[1] for x in shaders_output_nodes_props]
1859 valid = False
1860 if active:
1861 if (active.name != "Emission Viewer") and (active.type not in output_types):
1862 for out in active.outputs:
1863 if is_visible_socket(out):
1864 valid = True
1865 break
1866 if valid:
1867 # get material_output node
1868 materialout = None # placeholder node
1869 delete_sockets = []
1871 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1872 self.scan_nodes(base_node_tree, delete_sockets)
1874 materialout = self.get_shader_output_node(base_node_tree)
1875 if not materialout:
1876 materialout = base_node_tree.nodes.new(self.shader_output_ident)
1877 materialout.location = get_output_location(base_node_tree)
1878 materialout.select = False
1879 # Analyze outputs, add "Emission Viewer" if needed, make links
1880 out_i = None
1881 valid_outputs = []
1882 for i, out in enumerate(active.outputs):
1883 if is_visible_socket(out):
1884 valid_outputs.append(i)
1885 if valid_outputs:
1886 out_i = valid_outputs[0] # Start index of node's outputs
1887 for i, valid_i in enumerate(valid_outputs):
1888 for out_link in active.outputs[valid_i].links:
1889 if is_viewer_link(out_link, materialout):
1890 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1891 if i < len(valid_outputs) - 1:
1892 out_i = valid_outputs[i + 1]
1893 else:
1894 out_i = valid_outputs[0]
1896 make_links = [] # store sockets for new links
1897 delete_nodes = [] # store unused nodes to delete in the end
1898 if active.outputs:
1899 # If output type not 'SHADER' - "Emission Viewer" needed
1900 if active.outputs[out_i].type != 'SHADER':
1901 socket_type = 'NodeSocketColor'
1902 # get Emission Viewer node
1903 emission_exists = False
1904 emission_placeholder = base_node_tree.nodes[0]
1905 for node in base_node_tree.nodes:
1906 if "Emission Viewer" in node.name:
1907 emission_exists = True
1908 emission_placeholder = node
1909 if not emission_exists:
1910 emission = base_node_tree.nodes.new(self.shader_viewer_ident)
1911 emission.hide = True
1912 emission.location = [materialout.location.x, (materialout.location.y + 40)]
1913 emission.label = "Viewer"
1914 emission.name = "Emission Viewer"
1915 emission.use_custom_color = True
1916 emission.color = (0.6, 0.5, 0.4)
1917 emission.select = False
1918 else:
1919 emission = emission_placeholder
1920 output_socket = emission.inputs[0]
1922 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
1923 if emission.outputs[0].links.__len__() > 0:
1924 if not emission.outputs[0].links[0].to_node == materialout:
1925 make_links.append((emission.outputs[0], materialout.inputs[0]))
1926 else:
1927 make_links.append((emission.outputs[0], materialout.inputs[0]))
1929 # Set brightness of viewer to compensate for Film and CM exposure
1930 if context.scene.render.engine == 'CYCLES' and hasattr(context.scene, 'cycles'):
1931 intensity = 1/context.scene.cycles.film_exposure # Film exposure is a multiplier
1932 else:
1933 intensity = 1
1935 intensity /= pow(2, (context.scene.view_settings.exposure)) # CM exposure is measured in stops/EVs (2^x)
1936 emission.inputs[1].default_value = intensity
1938 else:
1939 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
1940 socket_type = 'NodeSocketShader'
1941 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
1942 make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
1943 output_socket = materialout.inputs[materialout_index]
1944 for node in base_node_tree.nodes:
1945 if node.name == 'Emission Viewer':
1946 delete_nodes.append((base_node_tree, node))
1947 for li_from, li_to in make_links:
1948 base_node_tree.links.new(li_from, li_to)
1950 # Crate links through node groups until we reach the active node
1951 tree = base_node_tree
1952 link_end = output_socket
1953 while tree.nodes.active != active:
1954 node = tree.nodes.active
1955 index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1956 link_start = node.outputs[index]
1957 node_socket = node.node_tree.outputs[index]
1958 if node_socket in delete_sockets:
1959 delete_sockets.remove(node_socket)
1960 tree.links.new(link_start, link_end)
1961 # Iterate
1962 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1963 tree = tree.nodes.active.node_tree
1964 tree.links.new(active.outputs[out_i], link_end)
1966 # Delete sockets
1967 for socket in delete_sockets:
1968 if not self.is_socket_used_other_mats(socket):
1969 tree = socket.id_data
1970 tree.outputs.remove(socket)
1972 # Delete nodes
1973 for tree, node in delete_nodes:
1974 tree.nodes.remove(node)
1976 nodes.active = active
1977 active.select = True
1979 force_update(context)
1981 return {'FINISHED'}
1982 else:
1983 return {'CANCELLED'}
1986 class NWFrameSelected(Operator, NWBase):
1987 bl_idname = "node.nw_frame_selected"
1988 bl_label = "Frame Selected"
1989 bl_description = "Add a frame node and parent the selected nodes to it"
1990 bl_options = {'REGISTER', 'UNDO'}
1992 label_prop: StringProperty(
1993 name='Label',
1994 description='The visual name of the frame node',
1995 default=' '
1997 color_prop: FloatVectorProperty(
1998 name="Color",
1999 description="The color of the frame node",
2000 default=(0.6, 0.6, 0.6),
2001 min=0, max=1, step=1, precision=3,
2002 subtype='COLOR_GAMMA', size=3
2005 def execute(self, context):
2006 nodes, links = get_nodes_links(context)
2007 selected = []
2008 for node in nodes:
2009 if node.select == True:
2010 selected.append(node)
2012 bpy.ops.node.add_node(type='NodeFrame')
2013 frm = nodes.active
2014 frm.label = self.label_prop
2015 frm.use_custom_color = True
2016 frm.color = self.color_prop
2018 for node in selected:
2019 node.parent = frm
2021 return {'FINISHED'}
2024 class NWReloadImages(Operator, NWBase):
2025 bl_idname = "node.nw_reload_images"
2026 bl_label = "Reload Images"
2027 bl_description = "Update all the image nodes to match their files on disk"
2029 def execute(self, context):
2030 nodes, links = get_nodes_links(context)
2031 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2032 num_reloaded = 0
2033 for node in nodes:
2034 if node.type in image_types:
2035 if node.type == "TEXTURE":
2036 if node.texture: # node has texture assigned
2037 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2038 if node.texture.image: # texture has image assigned
2039 node.texture.image.reload()
2040 num_reloaded += 1
2041 else:
2042 if node.image:
2043 node.image.reload()
2044 num_reloaded += 1
2046 if num_reloaded:
2047 self.report({'INFO'}, "Reloaded images")
2048 print("Reloaded " + str(num_reloaded) + " images")
2049 force_update(context)
2050 return {'FINISHED'}
2051 else:
2052 self.report({'WARNING'}, "No images found to reload in this node tree")
2053 return {'CANCELLED'}
2056 class NWSwitchNodeType(Operator, NWBase):
2057 """Switch type of selected nodes """
2058 bl_idname = "node.nw_swtch_node_type"
2059 bl_label = "Switch Node Type"
2060 bl_options = {'REGISTER', 'UNDO'}
2062 to_type: EnumProperty(
2063 name="Switch to type",
2064 items=list(shaders_input_nodes_props) +
2065 list(shaders_output_nodes_props) +
2066 list(shaders_shader_nodes_props) +
2067 list(shaders_texture_nodes_props) +
2068 list(shaders_color_nodes_props) +
2069 list(shaders_vector_nodes_props) +
2070 list(shaders_converter_nodes_props) +
2071 list(shaders_layout_nodes_props) +
2072 list(compo_input_nodes_props) +
2073 list(compo_output_nodes_props) +
2074 list(compo_color_nodes_props) +
2075 list(compo_converter_nodes_props) +
2076 list(compo_filter_nodes_props) +
2077 list(compo_vector_nodes_props) +
2078 list(compo_matte_nodes_props) +
2079 list(compo_distort_nodes_props) +
2080 list(compo_layout_nodes_props) +
2081 list(blender_mat_input_nodes_props) +
2082 list(blender_mat_output_nodes_props) +
2083 list(blender_mat_color_nodes_props) +
2084 list(blender_mat_vector_nodes_props) +
2085 list(blender_mat_converter_nodes_props) +
2086 list(blender_mat_layout_nodes_props) +
2087 list(texture_input_nodes_props) +
2088 list(texture_output_nodes_props) +
2089 list(texture_color_nodes_props) +
2090 list(texture_pattern_nodes_props) +
2091 list(texture_textures_nodes_props) +
2092 list(texture_converter_nodes_props) +
2093 list(texture_distort_nodes_props) +
2094 list(texture_layout_nodes_props)
2097 def execute(self, context):
2098 nodes, links = get_nodes_links(context)
2099 to_type = self.to_type
2100 # Those types of nodes will not swap.
2101 src_excludes = ('NodeFrame')
2102 # Those attributes of nodes will be copied if possible
2103 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
2104 'show_options', 'show_preview', 'show_texture',
2105 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2107 selected = [n for n in nodes if n.select]
2108 reselect = []
2109 for node in [n for n in selected if
2110 n.rna_type.identifier not in src_excludes and
2111 n.rna_type.identifier != to_type]:
2112 new_node = nodes.new(to_type)
2113 for attr in attrs_to_pass:
2114 if hasattr(node, attr) and hasattr(new_node, attr):
2115 setattr(new_node, attr, getattr(node, attr))
2116 # set image datablock of dst to image of src
2117 if hasattr(node, 'image') and hasattr(new_node, 'image'):
2118 if node.image:
2119 new_node.image = node.image
2120 # Special cases
2121 if new_node.type == 'SWITCH':
2122 new_node.hide = True
2123 # Dictionaries: src_sockets and dst_sockets:
2124 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2125 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2126 # in 'INPUTS' and 'OUTPUTS':
2127 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2128 # socket entry:
2129 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2130 src_sockets = {
2131 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2132 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2134 dst_sockets = {
2135 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2136 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2138 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2139 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2140 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2141 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
2142 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2143 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
2144 # enumerate in inputs, then in outputs
2145 # find name, default value and links of socket
2146 for i, socket in enumerate(in_out):
2147 the_name = socket.name
2148 dval = None
2149 # Not every socket, especially in outputs has "default_value"
2150 if hasattr(socket, 'default_value'):
2151 dval = socket.default_value
2152 socket_links = []
2153 for lnk in socket.links:
2154 socket_links.append(lnk)
2155 # check type of socket to fill proper keys.
2156 for the_type in types_order_one:
2157 if socket.type == the_type:
2158 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2159 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2160 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
2161 # Check which of the types in inputs/outputs is considered to be "main".
2162 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2163 for type_check in types_order_one:
2164 if sockets[in_out_name][type_check]:
2165 sockets[in_out_name]['MAIN'] = type_check
2166 break
2168 matches = {
2169 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2170 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2173 for inout, soctype in (
2174 ('INPUTS', 'MAIN',),
2175 ('INPUTS', 'SHADER',),
2176 ('INPUTS', 'RGBA',),
2177 ('INPUTS', 'VECTOR',),
2178 ('INPUTS', 'VALUE',),
2179 ('OUTPUTS', 'MAIN',),
2180 ('OUTPUTS', 'SHADER',),
2181 ('OUTPUTS', 'RGBA',),
2182 ('OUTPUTS', 'VECTOR',),
2183 ('OUTPUTS', 'VALUE',),
2185 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
2186 if soctype == 'MAIN':
2187 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
2188 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
2189 else:
2190 sc = src_sockets[inout][soctype]
2191 dt = dst_sockets[inout][soctype]
2192 # start with 'dt' to determine number of possibilities.
2193 for i, soc in enumerate(dt):
2194 # if src main has enough entries - match them with dst main sockets by indexes.
2195 if len(sc) > i:
2196 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
2197 # add 'VALUE_NAME' criterion to inputs.
2198 if inout == 'INPUTS' and soctype == 'VALUE':
2199 for s in sc:
2200 if s[2] == soc[2]: # if names match
2201 # append src (index, dval), dst (index, dval)
2202 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
2204 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2205 # This creates better links when relinking textures.
2206 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
2207 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
2209 # Pass default values and RELINK:
2210 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2211 # INPUTS: Base on matches in proper order.
2212 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
2213 # pass dvals
2214 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
2215 new_node.inputs[dst_i].default_value = src_dval
2216 # Special case: switch to math
2217 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2218 new_node.type == 'MATH' and\
2219 tp == 'MAIN':
2220 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
2221 new_node.inputs[dst_i].default_value = new_dst_dval
2222 if node.type == 'MIX_RGB':
2223 if node.blend_type in [o[0] for o in operations]:
2224 new_node.operation = node.blend_type
2225 # Special case: switch from math to some types
2226 if node.type == 'MATH' and\
2227 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2228 tp == 'MAIN':
2229 for i in range(3):
2230 new_node.inputs[dst_i].default_value[i] = src_dval
2231 if new_node.type == 'MIX_RGB':
2232 if node.operation in [t[0] for t in blend_types]:
2233 new_node.blend_type = node.operation
2234 # Set Fac of MIX_RGB to 1.0
2235 new_node.inputs[0].default_value = 1.0
2236 # make link only when dst matching input is not linked already.
2237 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
2238 in_src_link = node.inputs[src_i].links[0]
2239 in_dst_socket = new_node.inputs[dst_i]
2240 links.new(in_src_link.from_socket, in_dst_socket)
2241 links.remove(in_src_link)
2242 # OUTPUTS: Base on matches in proper order.
2243 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
2244 for out_src_link in node.outputs[src_i].links:
2245 out_dst_socket = new_node.outputs[dst_i]
2246 links.new(out_dst_socket, out_src_link.to_socket)
2247 # relink rest inputs if possible, no criteria
2248 for src_inp in node.inputs:
2249 for dst_inp in new_node.inputs:
2250 if src_inp.links and not dst_inp.links:
2251 src_link = src_inp.links[0]
2252 links.new(src_link.from_socket, dst_inp)
2253 links.remove(src_link)
2254 # relink rest outputs if possible, base on node kind if any left.
2255 for src_o in node.outputs:
2256 for out_src_link in src_o.links:
2257 for dst_o in new_node.outputs:
2258 if src_o.type == dst_o.type:
2259 links.new(dst_o, out_src_link.to_socket)
2260 # relink rest outputs no criteria if any left. Link all from first output.
2261 for src_o in node.outputs:
2262 for out_src_link in src_o.links:
2263 if new_node.outputs:
2264 links.new(new_node.outputs[0], out_src_link.to_socket)
2265 nodes.remove(node)
2266 force_update(context)
2267 return {'FINISHED'}
2270 class NWMergeNodes(Operator, NWBase):
2271 bl_idname = "node.nw_merge_nodes"
2272 bl_label = "Merge Nodes"
2273 bl_description = "Merge Selected Nodes"
2274 bl_options = {'REGISTER', 'UNDO'}
2276 mode: EnumProperty(
2277 name="mode",
2278 description="All possible blend types and math operations",
2279 items=blend_types + [op for op in operations if op not in blend_types],
2281 merge_type: EnumProperty(
2282 name="merge type",
2283 description="Type of Merge to be used",
2284 items=(
2285 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2286 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2287 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2288 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2289 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2290 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2294 def execute(self, context):
2295 settings = context.preferences.addons[__name__].preferences
2296 merge_hide = settings.merge_hide
2297 merge_position = settings.merge_position # 'center' or 'bottom'
2299 do_hide = False
2300 do_hide_shader = False
2301 if merge_hide == 'ALWAYS':
2302 do_hide = True
2303 do_hide_shader = True
2304 elif merge_hide == 'NON_SHADER':
2305 do_hide = True
2307 tree_type = context.space_data.node_tree.type
2308 if tree_type == 'COMPOSITING':
2309 node_type = 'CompositorNode'
2310 elif tree_type == 'SHADER':
2311 node_type = 'ShaderNode'
2312 elif tree_type == 'TEXTURE':
2313 node_type = 'TextureNode'
2314 nodes, links = get_nodes_links(context)
2315 mode = self.mode
2316 merge_type = self.merge_type
2317 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2318 # 'ZCOMBINE' works only if mode == 'MIX'
2319 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2320 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2321 merge_type = 'MIX'
2322 mode = 'MIX'
2323 selected_mix = [] # entry = [index, loc]
2324 selected_shader = [] # entry = [index, loc]
2325 selected_math = [] # entry = [index, loc]
2326 selected_z = [] # entry = [index, loc]
2327 selected_alphaover = [] # entry = [index, loc]
2329 for i, node in enumerate(nodes):
2330 if node.select and node.outputs:
2331 if merge_type == 'AUTO':
2332 for (type, types_list, dst) in (
2333 ('SHADER', ('MIX', 'ADD'), selected_shader),
2334 ('RGBA', [t[0] for t in blend_types], selected_mix),
2335 ('VALUE', [t[0] for t in operations], selected_math),
2337 output_type = node.outputs[0].type
2338 valid_mode = mode in types_list
2339 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2340 # Cheat that output type is 'RGBA',
2341 # and that 'MIX' exists in math operations list.
2342 # This way when selected_mix list is analyzed:
2343 # Node data will be appended even though it doesn't meet requirements.
2344 if output_type != 'SHADER' and mode == 'MIX':
2345 output_type = 'RGBA'
2346 valid_mode = True
2347 if output_type == type and valid_mode:
2348 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2349 else:
2350 for (type, types_list, dst) in (
2351 ('SHADER', ('MIX', 'ADD'), selected_shader),
2352 ('MIX', [t[0] for t in blend_types], selected_mix),
2353 ('MATH', [t[0] for t in operations], selected_math),
2354 ('ZCOMBINE', ('MIX', ), selected_z),
2355 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2357 if merge_type == type and mode in types_list:
2358 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2359 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2360 # use only 'Mix' nodes for merging.
2361 # For that we add selected_math list to selected_mix list and clear selected_math.
2362 if selected_mix and selected_math and merge_type == 'AUTO':
2363 selected_mix += selected_math
2364 selected_math = []
2366 for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
2367 if nodes_list:
2368 count_before = len(nodes)
2369 # sort list by loc_x - reversed
2370 nodes_list.sort(key=lambda k: k[1], reverse=True)
2371 # get maximum loc_x
2372 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2373 nodes_list.sort(key=lambda k: k[2], reverse=True)
2374 if merge_position == 'CENTER':
2375 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)
2376 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2377 if do_hide:
2378 loc_y += 40
2379 else:
2380 loc_y += 80
2381 else:
2382 loc_y = nodes_list[len(nodes_list) - 1][2]
2383 offset_y = 100
2384 if not do_hide:
2385 offset_y = 200
2386 if nodes_list == selected_shader and not do_hide_shader:
2387 offset_y = 150.0
2388 the_range = len(nodes_list) - 1
2389 if len(nodes_list) == 1:
2390 the_range = 1
2391 for i in range(the_range):
2392 if nodes_list == selected_mix:
2393 add_type = node_type + 'MixRGB'
2394 add = nodes.new(add_type)
2395 add.blend_type = mode
2396 if mode != 'MIX':
2397 add.inputs[0].default_value = 1.0
2398 add.show_preview = False
2399 add.hide = do_hide
2400 if do_hide:
2401 loc_y = loc_y - 50
2402 first = 1
2403 second = 2
2404 add.width_hidden = 100.0
2405 elif nodes_list == selected_math:
2406 add_type = node_type + 'Math'
2407 add = nodes.new(add_type)
2408 add.operation = mode
2409 add.hide = do_hide
2410 if do_hide:
2411 loc_y = loc_y - 50
2412 first = 0
2413 second = 1
2414 add.width_hidden = 100.0
2415 elif nodes_list == selected_shader:
2416 if mode == 'MIX':
2417 add_type = node_type + 'MixShader'
2418 add = nodes.new(add_type)
2419 add.hide = do_hide_shader
2420 if do_hide_shader:
2421 loc_y = loc_y - 50
2422 first = 1
2423 second = 2
2424 add.width_hidden = 100.0
2425 elif mode == 'ADD':
2426 add_type = node_type + 'AddShader'
2427 add = nodes.new(add_type)
2428 add.hide = do_hide_shader
2429 if do_hide_shader:
2430 loc_y = loc_y - 50
2431 first = 0
2432 second = 1
2433 add.width_hidden = 100.0
2434 elif nodes_list == selected_z:
2435 add = nodes.new('CompositorNodeZcombine')
2436 add.show_preview = False
2437 add.hide = do_hide
2438 if do_hide:
2439 loc_y = loc_y - 50
2440 first = 0
2441 second = 2
2442 add.width_hidden = 100.0
2443 elif nodes_list == selected_alphaover:
2444 add = nodes.new('CompositorNodeAlphaOver')
2445 add.show_preview = False
2446 add.hide = do_hide
2447 if do_hide:
2448 loc_y = loc_y - 50
2449 first = 1
2450 second = 2
2451 add.width_hidden = 100.0
2452 add.location = loc_x, loc_y
2453 loc_y += offset_y
2454 add.select = True
2455 count_adds = i + 1
2456 count_after = len(nodes)
2457 index = count_after - 1
2458 first_selected = nodes[nodes_list[0][0]]
2459 # "last" node has been added as first, so its index is count_before.
2460 last_add = nodes[count_before]
2461 # Special case:
2462 # Two nodes were selected and first selected has no output links, second selected has output links.
2463 # Then add links from last add to all links 'to_socket' of out links of second selected.
2464 if len(nodes_list) == 2:
2465 if not first_selected.outputs[0].links:
2466 second_selected = nodes[nodes_list[1][0]]
2467 for ss_link in second_selected.outputs[0].links:
2468 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2469 # Create list of invalid indexes.
2470 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2471 # Link only if "to_node" index not in invalid indexes list.
2472 if ss_link.to_node not in [nodes[i] for i in invalid_i]:
2473 links.new(last_add.outputs[0], ss_link.to_socket)
2474 # add links from last_add to all links 'to_socket' of out links of first selected.
2475 for fs_link in first_selected.outputs[0].links:
2476 # Prevent cyclic dependencies when nodes to be marged are linked to one another.
2477 # Create list of invalid indexes.
2478 invalid_i = [n[0] for n in (selected_mix + selected_math + selected_shader + selected_z)]
2479 # Link only if "to_node" index not in invalid indexes list.
2480 if fs_link.to_node not in [nodes[i] for i in invalid_i]:
2481 links.new(last_add.outputs[0], fs_link.to_socket)
2482 # add link from "first" selected and "first" add node
2483 node_to = nodes[count_after - 1]
2484 links.new(first_selected.outputs[0], node_to.inputs[first])
2485 if node_to.type == 'ZCOMBINE':
2486 for fs_out in first_selected.outputs:
2487 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
2488 links.new(fs_out, node_to.inputs[1])
2489 break
2490 # add links between added ADD nodes and between selected and ADD nodes
2491 for i in range(count_adds):
2492 if i < count_adds - 1:
2493 node_from = nodes[index]
2494 node_to = nodes[index - 1]
2495 node_to_input_i = first
2496 node_to_z_i = 1 # if z combine - link z to first z input
2497 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2498 if node_to.type == 'ZCOMBINE':
2499 for from_out in node_from.outputs:
2500 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2501 links.new(from_out, node_to.inputs[node_to_z_i])
2502 if len(nodes_list) > 1:
2503 node_from = nodes[nodes_list[i + 1][0]]
2504 node_to = nodes[index]
2505 node_to_input_i = second
2506 node_to_z_i = 3 # if z combine - link z to second z input
2507 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2508 if node_to.type == 'ZCOMBINE':
2509 for from_out in node_from.outputs:
2510 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2511 links.new(from_out, node_to.inputs[node_to_z_i])
2512 index -= 1
2513 # set "last" of added nodes as active
2514 nodes.active = last_add
2515 for i, x, y, dx, h in nodes_list:
2516 nodes[i].select = False
2518 return {'FINISHED'}
2521 class NWBatchChangeNodes(Operator, NWBase):
2522 bl_idname = "node.nw_batch_change"
2523 bl_label = "Batch Change"
2524 bl_description = "Batch Change Blend Type and Math Operation"
2525 bl_options = {'REGISTER', 'UNDO'}
2527 blend_type: EnumProperty(
2528 name="Blend Type",
2529 items=blend_types + navs,
2531 operation: EnumProperty(
2532 name="Operation",
2533 items=operations + navs,
2536 def execute(self, context):
2538 nodes, links = get_nodes_links(context)
2539 blend_type = self.blend_type
2540 operation = self.operation
2541 for node in context.selected_nodes:
2542 if node.type == 'MIX_RGB':
2543 if not blend_type in [nav[0] for nav in navs]:
2544 node.blend_type = blend_type
2545 else:
2546 if blend_type == 'NEXT':
2547 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2548 #index = blend_types.index(node.blend_type)
2549 if index == len(blend_types) - 1:
2550 node.blend_type = blend_types[0][0]
2551 else:
2552 node.blend_type = blend_types[index + 1][0]
2554 if blend_type == 'PREV':
2555 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2556 if index == 0:
2557 node.blend_type = blend_types[len(blend_types) - 1][0]
2558 else:
2559 node.blend_type = blend_types[index - 1][0]
2561 if node.type == 'MATH':
2562 if not operation in [nav[0] for nav in navs]:
2563 node.operation = operation
2564 else:
2565 if operation == 'NEXT':
2566 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2567 #index = operations.index(node.operation)
2568 if index == len(operations) - 1:
2569 node.operation = operations[0][0]
2570 else:
2571 node.operation = operations[index + 1][0]
2573 if operation == 'PREV':
2574 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2575 #index = operations.index(node.operation)
2576 if index == 0:
2577 node.operation = operations[len(operations) - 1][0]
2578 else:
2579 node.operation = operations[index - 1][0]
2581 return {'FINISHED'}
2584 class NWChangeMixFactor(Operator, NWBase):
2585 bl_idname = "node.nw_factor"
2586 bl_label = "Change Factor"
2587 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2588 bl_options = {'REGISTER', 'UNDO'}
2590 # option: Change factor.
2591 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2592 # Else - change factor by option value.
2593 option: FloatProperty()
2595 def execute(self, context):
2596 nodes, links = get_nodes_links(context)
2597 option = self.option
2598 selected = [] # entry = index
2599 for si, node in enumerate(nodes):
2600 if node.select:
2601 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2602 selected.append(si)
2604 for si in selected:
2605 fac = nodes[si].inputs[0]
2606 nodes[si].hide = False
2607 if option in {0.0, 1.0}:
2608 fac.default_value = option
2609 else:
2610 fac.default_value += option
2612 return {'FINISHED'}
2615 class NWCopySettings(Operator, NWBase):
2616 bl_idname = "node.nw_copy_settings"
2617 bl_label = "Copy Settings"
2618 bl_description = "Copy Settings of Active Node to Selected Nodes"
2619 bl_options = {'REGISTER', 'UNDO'}
2621 @classmethod
2622 def poll(cls, context):
2623 valid = False
2624 if nw_check(context):
2625 if (
2626 context.active_node is not None and
2627 context.active_node.type != 'FRAME'
2629 valid = True
2630 return valid
2632 def execute(self, context):
2633 node_active = context.active_node
2634 node_selected = context.selected_nodes
2636 # Error handling
2637 if not (len(node_selected) > 1):
2638 self.report({'ERROR'}, "2 nodes must be selected at least")
2639 return {'CANCELLED'}
2641 # Check if active node is in the selection
2642 selected_node_names = [n.name for n in node_selected]
2643 if node_active.name not in selected_node_names:
2644 self.report({'ERROR'}, "No active node")
2645 return {'CANCELLED'}
2647 # Get nodes in selection by type
2648 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2650 if not (len(valid_nodes) > 1) and node_active:
2651 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2652 return {'CANCELLED'}
2654 if len(valid_nodes) != len(node_selected):
2655 # Report nodes that are not valid
2656 valid_node_names = [n.name for n in valid_nodes]
2657 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2658 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2660 # Reference original
2661 orig = node_active
2662 #node_selected_names = [n.name for n in node_selected]
2664 # Output list
2665 success_names = []
2667 # Deselect all nodes
2668 for i in node_selected:
2669 i.select = False
2671 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2672 # Run through all other nodes
2673 for node in valid_nodes[1:]:
2675 # Check for frame node
2676 parent = node.parent if node.parent else None
2677 node_loc = [node.location.x, node.location.y]
2679 # Select original to duplicate
2680 orig.select = True
2682 # Duplicate selected node
2683 bpy.ops.node.duplicate()
2684 new_node = context.selected_nodes[0]
2686 # Deselect copy
2687 new_node.select = False
2689 # Properties to copy
2690 node_tree = node.id_data
2691 props_to_copy = 'bl_idname name location height width'.split(' ')
2693 # Input and outputs
2694 reconnections = []
2695 mappings = chain.from_iterable([node.inputs, node.outputs])
2696 for i in (i for i in mappings if i.is_linked):
2697 for L in i.links:
2698 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2700 # Properties
2701 props = {j: getattr(node, j) for j in props_to_copy}
2702 props_to_copy.pop(0)
2704 for prop in props_to_copy:
2705 setattr(new_node, prop, props[prop])
2707 # Get the node tree to remove the old node
2708 nodes = node_tree.nodes
2709 nodes.remove(node)
2710 new_node.name = props['name']
2712 if parent:
2713 new_node.parent = parent
2714 new_node.location = node_loc
2716 for str_from, str_to in reconnections:
2717 node_tree.links.new(eval(str_from), eval(str_to))
2719 success_names.append(new_node.name)
2721 orig.select = True
2722 node_tree.nodes.active = orig
2723 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2724 return {'FINISHED'}
2727 class NWCopyLabel(Operator, NWBase):
2728 bl_idname = "node.nw_copy_label"
2729 bl_label = "Copy Label"
2730 bl_options = {'REGISTER', 'UNDO'}
2732 option: EnumProperty(
2733 name="option",
2734 description="Source of name of label",
2735 items=(
2736 ('FROM_ACTIVE', 'from active', 'from active node',),
2737 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2738 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2742 def execute(self, context):
2743 nodes, links = get_nodes_links(context)
2744 option = self.option
2745 active = nodes.active
2746 if option == 'FROM_ACTIVE':
2747 if active:
2748 src_label = active.label
2749 for node in [n for n in nodes if n.select and nodes.active != n]:
2750 node.label = src_label
2751 elif option == 'FROM_NODE':
2752 selected = [n for n in nodes if n.select]
2753 for node in selected:
2754 for input in node.inputs:
2755 if input.links:
2756 src = input.links[0].from_node
2757 node.label = src.label
2758 break
2759 elif option == 'FROM_SOCKET':
2760 selected = [n for n in nodes if n.select]
2761 for node in selected:
2762 for input in node.inputs:
2763 if input.links:
2764 src = input.links[0].from_socket
2765 node.label = src.name
2766 break
2768 return {'FINISHED'}
2771 class NWClearLabel(Operator, NWBase):
2772 bl_idname = "node.nw_clear_label"
2773 bl_label = "Clear Label"
2774 bl_options = {'REGISTER', 'UNDO'}
2776 option: BoolProperty()
2778 def execute(self, context):
2779 nodes, links = get_nodes_links(context)
2780 for node in [n for n in nodes if n.select]:
2781 node.label = ''
2783 return {'FINISHED'}
2785 def invoke(self, context, event):
2786 if self.option:
2787 return self.execute(context)
2788 else:
2789 return context.window_manager.invoke_confirm(self, event)
2792 class NWModifyLabels(Operator, NWBase):
2793 """Modify Labels of all selected nodes"""
2794 bl_idname = "node.nw_modify_labels"
2795 bl_label = "Modify Labels"
2796 bl_options = {'REGISTER', 'UNDO'}
2798 prepend: StringProperty(
2799 name="Add to Beginning"
2801 append: StringProperty(
2802 name="Add to End"
2804 replace_from: StringProperty(
2805 name="Text to Replace"
2807 replace_to: StringProperty(
2808 name="Replace with"
2811 def execute(self, context):
2812 nodes, links = get_nodes_links(context)
2813 for node in [n for n in nodes if n.select]:
2814 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2816 return {'FINISHED'}
2818 def invoke(self, context, event):
2819 self.prepend = ""
2820 self.append = ""
2821 self.remove = ""
2822 return context.window_manager.invoke_props_dialog(self)
2825 class NWAddTextureSetup(Operator, NWBase):
2826 bl_idname = "node.nw_add_texture"
2827 bl_label = "Texture Setup"
2828 bl_description = "Add Texture Node Setup to Selected Shaders"
2829 bl_options = {'REGISTER', 'UNDO'}
2831 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2833 @classmethod
2834 def poll(cls, context):
2835 valid = False
2836 if nw_check(context):
2837 space = context.space_data
2838 if space.tree_type == 'ShaderNodeTree':
2839 valid = True
2840 return valid
2842 def execute(self, context):
2843 nodes, links = get_nodes_links(context)
2844 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
2845 texture_types = [x[1] for x in shaders_texture_nodes_props]
2846 selected_nodes = [n for n in nodes if n.select]
2847 for t_node in selected_nodes:
2848 valid = False
2849 input_index = 0
2850 if t_node.inputs:
2851 for index, i in enumerate(t_node.inputs):
2852 if not i.is_linked:
2853 valid = True
2854 input_index = index
2855 break
2856 if valid:
2857 locx = t_node.location.x
2858 locy = t_node.location.y - t_node.dimensions.y/2
2860 xoffset = [500, 700]
2861 is_texture = False
2862 if t_node.type in texture_types + ['MAPPING']:
2863 xoffset = [290, 500]
2864 is_texture = True
2866 coordout = 2
2867 image_type = 'ShaderNodeTexImage'
2869 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
2870 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
2871 if t_node.type == 'BACKGROUND':
2872 image_type = 'ShaderNodeTexEnvironment'
2874 if not is_texture:
2875 tex = nodes.new(image_type)
2876 tex.location = [locx - 200, locy + 112]
2877 nodes.active = tex
2878 links.new(tex.outputs[0], t_node.inputs[input_index])
2880 t_node.select = False
2881 if self.add_mapping or is_texture:
2882 if t_node.type != 'MAPPING':
2883 m = nodes.new('ShaderNodeMapping')
2884 m.location = [locx - xoffset[0], locy + 141]
2885 m.width = 240
2886 else:
2887 m = t_node
2888 coord = nodes.new('ShaderNodeTexCoord')
2889 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
2891 if not is_texture:
2892 links.new(m.outputs[0], tex.inputs[0])
2893 links.new(coord.outputs[coordout], m.inputs[0])
2894 else:
2895 nodes.active = m
2896 links.new(m.outputs[0], t_node.inputs[input_index])
2897 links.new(coord.outputs[coordout], m.inputs[0])
2898 else:
2899 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
2900 return {'FINISHED'}
2903 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2904 bl_idname = "node.nw_add_textures_for_principled"
2905 bl_label = "Principled Texture Setup"
2906 bl_description = "Add Texture Node Setup for Principled BSDF"
2907 bl_options = {'REGISTER', 'UNDO'}
2909 directory: StringProperty(
2910 name='Directory',
2911 subtype='DIR_PATH',
2912 default='',
2913 description='Folder to search in for image files'
2915 files: CollectionProperty(
2916 type=bpy.types.OperatorFileListElement,
2917 options={'HIDDEN', 'SKIP_SAVE'}
2920 relative_path: BoolProperty(
2921 name='Relative Path',
2922 description='Select the file relative to the blend file',
2923 default=True
2926 order = [
2927 "filepath",
2928 "files",
2931 def draw(self, context):
2932 layout = self.layout
2933 layout.alignment = 'LEFT'
2935 layout.prop(self, 'relative_path')
2937 @classmethod
2938 def poll(cls, context):
2939 valid = False
2940 if nw_check(context):
2941 space = context.space_data
2942 if space.tree_type == 'ShaderNodeTree':
2943 valid = True
2944 return valid
2946 def execute(self, context):
2947 # Check if everything is ok
2948 if not self.directory:
2949 self.report({'INFO'}, 'No Folder Selected')
2950 return {'CANCELLED'}
2951 if not self.files[:]:
2952 self.report({'INFO'}, 'No Files Selected')
2953 return {'CANCELLED'}
2955 nodes, links = get_nodes_links(context)
2956 active_node = nodes.active
2957 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
2958 self.report({'INFO'}, 'Select Principled BSDF')
2959 return {'CANCELLED'}
2961 # Helper_functions
2962 def split_into__components(fname):
2963 # Split filename into components
2964 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2965 # Remove extension
2966 fname = path.splitext(fname)[0]
2967 # Remove digits
2968 fname = ''.join(i for i in fname if not i.isdigit())
2969 # Separate CamelCase by space
2970 fname = re.sub("([a-z])([A-Z])","\g<1> \g<2>",fname)
2971 # Replace common separators with SPACE
2972 seperators = ['_', '.', '-', '__', '--', '#']
2973 for sep in seperators:
2974 fname = fname.replace(sep, ' ')
2976 components = fname.split(' ')
2977 components = [c.lower() for c in components]
2978 return components
2980 # Filter textures names for texturetypes in filenames
2981 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2982 tags = context.preferences.addons[__name__].preferences.principled_tags
2983 normal_abbr = tags.normal.split(' ')
2984 bump_abbr = tags.bump.split(' ')
2985 gloss_abbr = tags.gloss.split(' ')
2986 rough_abbr = tags.rough.split(' ')
2987 socketnames = [
2988 ['Displacement', tags.displacement.split(' '), None],
2989 ['Base Color', tags.base_color.split(' '), None],
2990 ['Subsurface Color', tags.sss_color.split(' '), None],
2991 ['Metallic', tags.metallic.split(' '), None],
2992 ['Specular', tags.specular.split(' '), None],
2993 ['Roughness', rough_abbr + gloss_abbr, None],
2994 ['Normal', normal_abbr + bump_abbr, None],
2997 # Look through texture_types and set value as filename of first matched file
2998 def match_files_to_socket_names():
2999 for sname in socketnames:
3000 for file in self.files:
3001 fname = file.name
3002 filenamecomponents = split_into__components(fname)
3003 matches = set(sname[1]).intersection(set(filenamecomponents))
3004 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3005 if matches:
3006 sname[2] = fname
3007 break
3009 match_files_to_socket_names()
3010 # Remove socketnames without found files
3011 socketnames = [s for s in socketnames if s[2]
3012 and path.exists(self.directory+s[2])]
3013 if not socketnames:
3014 self.report({'INFO'}, 'No matching images found')
3015 print('No matching images found')
3016 return {'CANCELLED'}
3018 # Don't override path earlier as os.path is used to check the absolute path
3019 import_path = self.directory
3020 if self.relative_path:
3021 if bpy.data.filepath:
3022 import_path = bpy.path.relpath(self.directory)
3023 else:
3024 self.report({'WARNING'}, 'Relative paths cannot be used with unsaved scenes!')
3025 print('Relative paths cannot be used with unsaved scenes!')
3027 # Add found images
3028 print('\nMatched Textures:')
3029 texture_nodes = []
3030 disp_texture = None
3031 normal_node = None
3032 roughness_node = None
3033 for i, sname in enumerate(socketnames):
3034 print(i, sname[0], sname[2])
3036 # DISPLACEMENT NODES
3037 if sname[0] == 'Displacement':
3038 disp_texture = nodes.new(type='ShaderNodeTexImage')
3039 img = bpy.data.images.load(path.join(import_path, sname[2]))
3040 disp_texture.image = img
3041 disp_texture.label = 'Displacement'
3042 if disp_texture.image:
3043 disp_texture.image.colorspace_settings.is_data = True
3045 # Add displacement offset nodes
3046 disp_node = nodes.new(type='ShaderNodeDisplacement')
3047 disp_node.location = active_node.location + Vector((0, -560))
3048 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
3050 # TODO Turn on true displacement in the material
3051 # Too complicated for now
3053 # Find output node
3054 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
3055 if output_node:
3056 if not output_node[0].inputs[2].is_linked:
3057 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
3059 continue
3061 if not active_node.inputs[sname[0]].is_linked:
3062 # No texture node connected -> add texture node with new image
3063 texture_node = nodes.new(type='ShaderNodeTexImage')
3064 img = bpy.data.images.load(path.join(import_path, sname[2]))
3065 texture_node.image = img
3067 # NORMAL NODES
3068 if sname[0] == 'Normal':
3069 # Test if new texture node is normal or bump map
3070 fname_components = split_into__components(sname[2])
3071 match_normal = set(normal_abbr).intersection(set(fname_components))
3072 match_bump = set(bump_abbr).intersection(set(fname_components))
3073 if match_normal:
3074 # If Normal add normal node in between
3075 normal_node = nodes.new(type='ShaderNodeNormalMap')
3076 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
3077 elif match_bump:
3078 # If Bump add bump node in between
3079 normal_node = nodes.new(type='ShaderNodeBump')
3080 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
3082 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
3083 normal_node_texture = texture_node
3085 elif sname[0] == 'Roughness':
3086 # Test if glossy or roughness map
3087 fname_components = split_into__components(sname[2])
3088 match_rough = set(rough_abbr).intersection(set(fname_components))
3089 match_gloss = set(gloss_abbr).intersection(set(fname_components))
3091 if match_rough:
3092 # If Roughness nothing to to
3093 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
3095 elif match_gloss:
3096 # If Gloss Map add invert node
3097 invert_node = nodes.new(type='ShaderNodeInvert')
3098 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
3100 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
3101 roughness_node = texture_node
3103 else:
3104 # This is a simple connection Texture --> Input slot
3105 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
3107 # Use non-color for all but 'Base Color' Textures
3108 if not sname[0] in ['Base Color'] and texture_node.image:
3109 texture_node.image.colorspace_settings.is_data = True
3111 else:
3112 # If already texture connected. add to node list for alignment
3113 texture_node = active_node.inputs[sname[0]].links[0].from_node
3115 # This are all connected texture nodes
3116 texture_nodes.append(texture_node)
3117 texture_node.label = sname[0]
3119 if disp_texture:
3120 texture_nodes.append(disp_texture)
3122 # Alignment
3123 for i, texture_node in enumerate(texture_nodes):
3124 offset = Vector((-550, (i * -280) + 200))
3125 texture_node.location = active_node.location + offset
3127 if normal_node:
3128 # Extra alignment if normal node was added
3129 normal_node.location = normal_node_texture.location + Vector((300, 0))
3131 if roughness_node:
3132 # Alignment of invert node if glossy map
3133 invert_node.location = roughness_node.location + Vector((300, 0))
3135 # Add texture input + mapping
3136 mapping = nodes.new(type='ShaderNodeMapping')
3137 mapping.location = active_node.location + Vector((-1050, 0))
3138 if len(texture_nodes) > 1:
3139 # If more than one texture add reroute node in between
3140 reroute = nodes.new(type='NodeReroute')
3141 texture_nodes.append(reroute)
3142 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
3143 reroute.location = tex_coords + Vector((-50, -120))
3144 for texture_node in texture_nodes:
3145 link = links.new(texture_node.inputs[0], reroute.outputs[0])
3146 link = links.new(reroute.inputs[0], mapping.outputs[0])
3147 else:
3148 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
3150 # Connect texture_coordiantes to mapping node
3151 texture_input = nodes.new(type='ShaderNodeTexCoord')
3152 texture_input.location = mapping.location + Vector((-200, 0))
3153 link = links.new(mapping.inputs[0], texture_input.outputs[2])
3155 # Create frame around tex coords and mapping
3156 frame = nodes.new(type='NodeFrame')
3157 frame.label = 'Mapping'
3158 mapping.parent = frame
3159 texture_input.parent = frame
3160 frame.update()
3162 # Create frame around texture nodes
3163 frame = nodes.new(type='NodeFrame')
3164 frame.label = 'Textures'
3165 for tnode in texture_nodes:
3166 tnode.parent = frame
3167 frame.update()
3169 # Just to be sure
3170 active_node.select = False
3171 nodes.update()
3172 links.update()
3173 force_update(context)
3174 return {'FINISHED'}
3177 class NWAddReroutes(Operator, NWBase):
3178 """Add Reroute Nodes and link them to outputs of selected nodes"""
3179 bl_idname = "node.nw_add_reroutes"
3180 bl_label = "Add Reroutes"
3181 bl_description = "Add Reroutes to Outputs"
3182 bl_options = {'REGISTER', 'UNDO'}
3184 option: EnumProperty(
3185 name="option",
3186 items=[
3187 ('ALL', 'to all', 'Add to all outputs'),
3188 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3189 ('LINKED', 'to linked', 'Add only to linked outputs'),
3193 def execute(self, context):
3194 tree_type = context.space_data.node_tree.type
3195 option = self.option
3196 nodes, links = get_nodes_links(context)
3197 # output valid when option is 'all' or when 'loose' output has no links
3198 valid = False
3199 post_select = [] # nodes to be selected after execution
3200 # create reroutes and recreate links
3201 for node in [n for n in nodes if n.select]:
3202 if node.outputs:
3203 x = node.location.x
3204 y = node.location.y
3205 width = node.width
3206 # unhide 'REROUTE' nodes to avoid issues with location.y
3207 if node.type == 'REROUTE':
3208 node.hide = False
3209 # When node is hidden - width_hidden not usable.
3210 # Hack needed to calculate real width
3211 if node.hide:
3212 bpy.ops.node.select_all(action='DESELECT')
3213 helper = nodes.new('NodeReroute')
3214 helper.select = True
3215 node.select = True
3216 # resize node and helper to zero. Then check locations to calculate width
3217 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
3218 width = 2.0 * (helper.location.x - node.location.x)
3219 # restore node location
3220 node.location = x, y
3221 # delete helper
3222 node.select = False
3223 # only helper is selected now
3224 bpy.ops.node.delete()
3225 x = node.location.x + width + 20.0
3226 if node.type != 'REROUTE':
3227 y -= 35.0
3228 y_offset = -22.0
3229 loc = x, y
3230 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
3231 for out_i, output in enumerate(node.outputs):
3232 pass_used = False # initial value to be analyzed if 'R_LAYERS'
3233 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3234 if node.type != 'R_LAYERS':
3235 pass_used = True
3236 else: # if 'R_LAYERS' check if output represent used render pass
3237 node_scene = node.scene
3238 node_layer = node.layer
3239 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3240 if output.name == 'Alpha':
3241 pass_used = True
3242 else:
3243 # check entries in global 'rl_outputs' variable
3244 for rlo in rl_outputs:
3245 if output.name in {rlo.output_name, rlo.exr_output_name}:
3246 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
3247 break
3248 if pass_used:
3249 valid = ((option == 'ALL') or
3250 (option == 'LOOSE' and not output.links) or
3251 (option == 'LINKED' and output.links))
3252 # Add reroutes only if valid, but offset location in all cases.
3253 if valid:
3254 n = nodes.new('NodeReroute')
3255 nodes.active = n
3256 for link in output.links:
3257 links.new(n.outputs[0], link.to_socket)
3258 links.new(output, n.inputs[0])
3259 n.location = loc
3260 post_select.append(n)
3261 reroutes_count += 1
3262 y += y_offset
3263 loc = x, y
3264 # disselect the node so that after execution of script only newly created nodes are selected
3265 node.select = False
3266 # nicer reroutes distribution along y when node.hide
3267 if node.hide:
3268 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
3269 for reroute in [r for r in nodes if r.select]:
3270 reroute.location.y -= y_translate
3271 for node in post_select:
3272 node.select = True
3274 return {'FINISHED'}
3277 class NWLinkActiveToSelected(Operator, NWBase):
3278 """Link active node to selected nodes basing on various criteria"""
3279 bl_idname = "node.nw_link_active_to_selected"
3280 bl_label = "Link Active Node to Selected"
3281 bl_options = {'REGISTER', 'UNDO'}
3283 replace: BoolProperty()
3284 use_node_name: BoolProperty()
3285 use_outputs_names: BoolProperty()
3287 @classmethod
3288 def poll(cls, context):
3289 valid = False
3290 if nw_check(context):
3291 if context.active_node is not None:
3292 if context.active_node.select:
3293 valid = True
3294 return valid
3296 def execute(self, context):
3297 nodes, links = get_nodes_links(context)
3298 replace = self.replace
3299 use_node_name = self.use_node_name
3300 use_outputs_names = self.use_outputs_names
3301 active = nodes.active
3302 selected = [node for node in nodes if node.select and node != active]
3303 outputs = [] # Only usable outputs of active nodes will be stored here.
3304 for out in active.outputs:
3305 if active.type != 'R_LAYERS':
3306 outputs.append(out)
3307 else:
3308 # 'R_LAYERS' node type needs special handling.
3309 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3310 # Only outputs that represent used passes should be taken into account
3311 # Check if pass represented by output is used.
3312 # global 'rl_outputs' list will be used for that
3313 for rlo in rl_outputs:
3314 pass_used = False # initial value. Will be set to True if pass is used
3315 if out.name == 'Alpha':
3316 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3317 pass_used = True
3318 elif out.name in {rlo.output_name, rlo.exr_output_name}:
3319 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3320 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
3321 break
3322 if pass_used:
3323 outputs.append(out)
3324 doit = True # Will be changed to False when links successfully added to previous output.
3325 for out in outputs:
3326 if doit:
3327 for node in selected:
3328 dst_name = node.name # Will be compared with src_name if needed.
3329 # When node has label - use it as dst_name
3330 if node.label:
3331 dst_name = node.label
3332 valid = True # Initial value. Will be changed to False if names don't match.
3333 src_name = dst_name # If names not used - this asignment will keep valid = True.
3334 if use_node_name:
3335 # Set src_name to source node name or label
3336 src_name = active.name
3337 if active.label:
3338 src_name = active.label
3339 elif use_outputs_names:
3340 src_name = (out.name, )
3341 for rlo in rl_outputs:
3342 if out.name in {rlo.output_name, rlo.exr_output_name}:
3343 src_name = (rlo.output_name, rlo.exr_output_name)
3344 if dst_name not in src_name:
3345 valid = False
3346 if valid:
3347 for input in node.inputs:
3348 if input.type == out.type or node.type == 'REROUTE':
3349 if replace or not input.is_linked:
3350 links.new(out, input)
3351 if not use_node_name and not use_outputs_names:
3352 doit = False
3353 break
3355 return {'FINISHED'}
3358 class NWAlignNodes(Operator, NWBase):
3359 '''Align the selected nodes neatly in a row/column'''
3360 bl_idname = "node.nw_align_nodes"
3361 bl_label = "Align Nodes"
3362 bl_options = {'REGISTER', 'UNDO'}
3363 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3365 def execute(self, context):
3366 nodes, links = get_nodes_links(context)
3367 margin = self.margin
3369 selection = []
3370 for node in nodes:
3371 if node.select and node.type != 'FRAME':
3372 selection.append(node)
3374 # If no nodes are selected, align all nodes
3375 active_loc = None
3376 if not selection:
3377 selection = nodes
3378 elif nodes.active in selection:
3379 active_loc = copy(nodes.active.location) # make a copy, not a reference
3381 # Check if nodes should be laid out horizontally or vertically
3382 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3383 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3384 x_range = max(x_locs) - min(x_locs)
3385 y_range = max(y_locs) - min(y_locs)
3386 mid_x = (max(x_locs) + min(x_locs)) / 2
3387 mid_y = (max(y_locs) + min(y_locs)) / 2
3388 horizontal = x_range > y_range
3390 # Sort selection by location of node mid-point
3391 if horizontal:
3392 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3393 else:
3394 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3396 # Alignment
3397 current_pos = 0
3398 for node in selection:
3399 current_margin = margin
3400 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3402 if horizontal:
3403 node.location.x = current_pos
3404 current_pos += current_margin + node.dimensions.x
3405 node.location.y = mid_y + (node.dimensions.y / 2)
3406 else:
3407 node.location.y = current_pos
3408 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3409 node.location.x = mid_x - (node.dimensions.x / 2)
3411 # If active node is selected, center nodes around it
3412 if active_loc is not None:
3413 active_loc_diff = active_loc - nodes.active.location
3414 for node in selection:
3415 node.location += active_loc_diff
3416 else: # Position nodes centered around where they used to be
3417 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])
3418 new_mid = (max(locs) + min(locs)) / 2
3419 for node in selection:
3420 if horizontal:
3421 node.location.x += (mid_x - new_mid)
3422 else:
3423 node.location.y += (mid_y - new_mid)
3425 return {'FINISHED'}
3428 class NWSelectParentChildren(Operator, NWBase):
3429 bl_idname = "node.nw_select_parent_child"
3430 bl_label = "Select Parent or Children"
3431 bl_options = {'REGISTER', 'UNDO'}
3433 option: EnumProperty(
3434 name="option",
3435 items=(
3436 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3437 ('CHILD', 'Select Children', 'Select members of selected frame'),
3441 def execute(self, context):
3442 nodes, links = get_nodes_links(context)
3443 option = self.option
3444 selected = [node for node in nodes if node.select]
3445 if option == 'PARENT':
3446 for sel in selected:
3447 parent = sel.parent
3448 if parent:
3449 parent.select = True
3450 else: # option == 'CHILD'
3451 for sel in selected:
3452 children = [node for node in nodes if node.parent == sel]
3453 for kid in children:
3454 kid.select = True
3456 return {'FINISHED'}
3459 class NWDetachOutputs(Operator, NWBase):
3460 """Detach outputs of selected node leaving inputs linked"""
3461 bl_idname = "node.nw_detach_outputs"
3462 bl_label = "Detach Outputs"
3463 bl_options = {'REGISTER', 'UNDO'}
3465 def execute(self, context):
3466 nodes, links = get_nodes_links(context)
3467 selected = context.selected_nodes
3468 bpy.ops.node.duplicate_move_keep_inputs()
3469 new_nodes = context.selected_nodes
3470 bpy.ops.node.select_all(action="DESELECT")
3471 for node in selected:
3472 node.select = True
3473 bpy.ops.node.delete_reconnect()
3474 for new_node in new_nodes:
3475 new_node.select = True
3476 bpy.ops.transform.translate('INVOKE_DEFAULT')
3478 return {'FINISHED'}
3481 class NWLinkToOutputNode(Operator, NWBase):
3482 """Link to Composite node or Material Output node"""
3483 bl_idname = "node.nw_link_out"
3484 bl_label = "Connect to Output"
3485 bl_options = {'REGISTER', 'UNDO'}
3487 @classmethod
3488 def poll(cls, context):
3489 valid = False
3490 if nw_check(context):
3491 if context.active_node is not None:
3492 for out in context.active_node.outputs:
3493 if is_visible_socket(out):
3494 valid = True
3495 break
3496 return valid
3498 def execute(self, context):
3499 nodes, links = get_nodes_links(context)
3500 active = nodes.active
3501 output_node = None
3502 output_index = None
3503 tree_type = context.space_data.tree_type
3504 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
3505 output_types_compo = ['COMPOSITE']
3506 output_types_blender_mat = ['OUTPUT']
3507 output_types_textures = ['OUTPUT']
3508 output_types = output_types_shaders + output_types_compo + output_types_blender_mat
3509 for node in nodes:
3510 if node.type in output_types:
3511 output_node = node
3512 break
3513 if not output_node:
3514 bpy.ops.node.select_all(action="DESELECT")
3515 if tree_type == 'ShaderNodeTree':
3516 output_node = nodes.new('ShaderNodeOutputMaterial')
3517 elif tree_type == 'CompositorNodeTree':
3518 output_node = nodes.new('CompositorNodeComposite')
3519 elif tree_type == 'TextureNodeTree':
3520 output_node = nodes.new('TextureNodeOutput')
3521 output_node.location.x = active.location.x + active.dimensions.x + 80
3522 output_node.location.y = active.location.y
3523 if (output_node and active.outputs):
3524 for i, output in enumerate(active.outputs):
3525 if is_visible_socket(output):
3526 output_index = i
3527 break
3528 for i, output in enumerate(active.outputs):
3529 if output.type == output_node.inputs[0].type and is_visible_socket(output):
3530 output_index = i
3531 break
3533 out_input_index = 0
3534 if tree_type == 'ShaderNodeTree':
3535 if active.outputs[output_index].name == 'Volume':
3536 out_input_index = 1
3537 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3538 out_input_index = 2
3539 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3541 force_update(context) # viewport render does not update
3543 return {'FINISHED'}
3546 class NWMakeLink(Operator, NWBase):
3547 """Make a link from one socket to another"""
3548 bl_idname = 'node.nw_make_link'
3549 bl_label = 'Make Link'
3550 bl_options = {'REGISTER', 'UNDO'}
3551 from_socket: IntProperty()
3552 to_socket: IntProperty()
3554 def execute(self, context):
3555 nodes, links = get_nodes_links(context)
3557 n1 = nodes[context.scene.NWLazySource]
3558 n2 = nodes[context.scene.NWLazyTarget]
3560 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3562 force_update(context)
3564 return {'FINISHED'}
3567 class NWCallInputsMenu(Operator, NWBase):
3568 """Link from this output"""
3569 bl_idname = 'node.nw_call_inputs_menu'
3570 bl_label = 'Make Link'
3571 bl_options = {'REGISTER', 'UNDO'}
3572 from_socket: IntProperty()
3574 def execute(self, context):
3575 nodes, links = get_nodes_links(context)
3577 context.scene.NWSourceSocket = self.from_socket
3579 n1 = nodes[context.scene.NWLazySource]
3580 n2 = nodes[context.scene.NWLazyTarget]
3581 if len(n2.inputs) > 1:
3582 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3583 elif len(n2.inputs) == 1:
3584 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3585 return {'FINISHED'}
3588 class NWAddSequence(Operator, NWBase, ImportHelper):
3589 """Add an Image Sequence"""
3590 bl_idname = 'node.nw_add_sequence'
3591 bl_label = 'Import Image Sequence'
3592 bl_options = {'REGISTER', 'UNDO'}
3594 directory: StringProperty(
3595 subtype="DIR_PATH"
3597 filename: StringProperty(
3598 subtype="FILE_NAME"
3600 files: CollectionProperty(
3601 type=bpy.types.OperatorFileListElement,
3602 options={'HIDDEN', 'SKIP_SAVE'}
3605 def execute(self, context):
3606 nodes, links = get_nodes_links(context)
3607 directory = self.directory
3608 filename = self.filename
3609 files = self.files
3610 tree = context.space_data.node_tree
3612 # DEBUG
3613 # print ("\nDIR:", directory)
3614 # print ("FN:", filename)
3615 # print ("Fs:", list(f.name for f in files), '\n')
3617 if tree.type == 'SHADER':
3618 node_type = "ShaderNodeTexImage"
3619 elif tree.type == 'COMPOSITING':
3620 node_type = "CompositorNodeImage"
3621 else:
3622 self.report({'ERROR'}, "Unsupported Node Tree type!")
3623 return {'CANCELLED'}
3625 if not files[0].name and not filename:
3626 self.report({'ERROR'}, "No file chosen")
3627 return {'CANCELLED'}
3628 elif files[0].name and (not filename or not path.exists(directory+filename)):
3629 # User has selected multiple files without an active one, or the active one is non-existant
3630 filename = files[0].name
3632 if not path.exists(directory+filename):
3633 self.report({'ERROR'}, filename+" does not exist!")
3634 return {'CANCELLED'}
3636 without_ext = '.'.join(filename.split('.')[:-1])
3638 # if last digit isn't a number, it's not a sequence
3639 if not without_ext[-1].isdigit():
3640 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3641 return {'CANCELLED'}
3644 extension = filename.split('.')[-1]
3645 reverse = without_ext[::-1] # reverse string
3647 count_numbers = 0
3648 for char in reverse:
3649 if char.isdigit():
3650 count_numbers += 1
3651 else:
3652 break
3654 without_num = without_ext[:count_numbers*-1]
3656 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3658 num_frames = len(files)
3660 nodes_list = [node for node in nodes]
3661 if nodes_list:
3662 nodes_list.sort(key=lambda k: k.location.x)
3663 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3664 yloc = 0
3665 for node in nodes:
3666 node.select = False
3667 yloc += node_mid_pt(node, 'y')
3668 yloc = yloc/len(nodes)
3669 else:
3670 xloc = 0
3671 yloc = 0
3673 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3675 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3676 node = nodes.active
3677 node.label = name_with_hashes
3679 img = bpy.data.images.load(directory+(without_ext+'.'+extension))
3680 img.source = 'SEQUENCE'
3681 img.name = name_with_hashes
3682 node.image = img
3683 image_user = node.image_user if tree.type == 'SHADER' else node
3684 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
3685 image_user.frame_duration = num_frames
3687 return {'FINISHED'}
3690 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
3691 """Add multiple images at once"""
3692 bl_idname = 'node.nw_add_multiple_images'
3693 bl_label = 'Open Selected Images'
3694 bl_options = {'REGISTER', 'UNDO'}
3695 directory: StringProperty(
3696 subtype="DIR_PATH"
3698 files: CollectionProperty(
3699 type=bpy.types.OperatorFileListElement,
3700 options={'HIDDEN', 'SKIP_SAVE'}
3703 def execute(self, context):
3704 nodes, links = get_nodes_links(context)
3706 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3708 if context.space_data.node_tree.type == 'SHADER':
3709 node_type = "ShaderNodeTexImage"
3710 elif context.space_data.node_tree.type == 'COMPOSITING':
3711 node_type = "CompositorNodeImage"
3712 else:
3713 self.report({'ERROR'}, "Unsupported Node Tree type!")
3714 return {'CANCELLED'}
3716 new_nodes = []
3717 for f in self.files:
3718 fname = f.name
3720 node = nodes.new(node_type)
3721 new_nodes.append(node)
3722 node.label = fname
3723 node.hide = True
3724 node.width_hidden = 100
3725 node.location.x = xloc
3726 node.location.y = yloc
3727 yloc -= 40
3729 img = bpy.data.images.load(self.directory+fname)
3730 node.image = img
3732 # shift new nodes up to center of tree
3733 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3734 for node in nodes:
3735 if node in new_nodes:
3736 node.select = True
3737 node.location.y += (list_size/2)
3738 else:
3739 node.select = False
3740 return {'FINISHED'}
3743 class NWViewerFocus(bpy.types.Operator):
3744 """Set the viewer tile center to the mouse position"""
3745 bl_idname = "node.nw_viewer_focus"
3746 bl_label = "Viewer Focus"
3748 x: bpy.props.IntProperty()
3749 y: bpy.props.IntProperty()
3751 @classmethod
3752 def poll(cls, context):
3753 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3755 def execute(self, context):
3756 return {'FINISHED'}
3758 def invoke(self, context, event):
3759 render = context.scene.render
3760 space = context.space_data
3761 percent = render.resolution_percentage*0.01
3763 nodes, links = get_nodes_links(context)
3764 viewers = [n for n in nodes if n.type == 'VIEWER']
3766 if viewers:
3767 mlocx = event.mouse_region_x
3768 mlocy = event.mouse_region_y
3769 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
3771 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3772 region_x = context.region.width
3773 region_y = context.region.height
3775 region_center_x = context.region.width / 2
3776 region_center_y = context.region.height / 2
3778 bd_x = render.resolution_x * percent * space.backdrop_zoom
3779 bd_y = render.resolution_y * percent * space.backdrop_zoom
3781 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
3782 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
3784 margin_x = region_center_x - backdrop_center_x
3785 margin_y = region_center_y - backdrop_center_y
3787 abs_mouse_x = (mlocx - margin_x) / bd_x
3788 abs_mouse_y = (mlocy - margin_y) / bd_y
3790 for node in viewers:
3791 node.center_x = abs_mouse_x
3792 node.center_y = abs_mouse_y
3793 else:
3794 return {'PASS_THROUGH'}
3796 return self.execute(context)
3799 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3800 """Save the current viewer node to an image file"""
3801 bl_idname = "node.nw_save_viewer"
3802 bl_label = "Save This Image"
3803 filepath: StringProperty(subtype="FILE_PATH")
3804 filename_ext: EnumProperty(
3805 name="Format",
3806 description="Choose the file format to save to",
3807 items=(('.bmp', "BMP", ""),
3808 ('.rgb', 'IRIS', ""),
3809 ('.png', 'PNG', ""),
3810 ('.jpg', 'JPEG', ""),
3811 ('.jp2', 'JPEG2000', ""),
3812 ('.tga', 'TARGA', ""),
3813 ('.cin', 'CINEON', ""),
3814 ('.dpx', 'DPX', ""),
3815 ('.exr', 'OPEN_EXR', ""),
3816 ('.hdr', 'HDR', ""),
3817 ('.tif', 'TIFF', "")),
3818 default='.png',
3821 @classmethod
3822 def poll(cls, context):
3823 valid = False
3824 if nw_check(context):
3825 if context.space_data.tree_type == 'CompositorNodeTree':
3826 if "Viewer Node" in [i.name for i in bpy.data.images]:
3827 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3828 valid = True
3829 return valid
3831 def execute(self, context):
3832 fp = self.filepath
3833 if fp:
3834 formats = {
3835 '.bmp': 'BMP',
3836 '.rgb': 'IRIS',
3837 '.png': 'PNG',
3838 '.jpg': 'JPEG',
3839 '.jpeg': 'JPEG',
3840 '.jp2': 'JPEG2000',
3841 '.tga': 'TARGA',
3842 '.cin': 'CINEON',
3843 '.dpx': 'DPX',
3844 '.exr': 'OPEN_EXR',
3845 '.hdr': 'HDR',
3846 '.tiff': 'TIFF',
3847 '.tif': 'TIFF'}
3848 basename, ext = path.splitext(fp)
3849 old_render_format = context.scene.render.image_settings.file_format
3850 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3851 context.area.type = "IMAGE_EDITOR"
3852 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3853 context.area.spaces[0].image.save_render(fp)
3854 context.area.type = "NODE_EDITOR"
3855 context.scene.render.image_settings.file_format = old_render_format
3856 return {'FINISHED'}
3859 class NWResetNodes(bpy.types.Operator):
3860 """Reset Nodes in Selection"""
3861 bl_idname = "node.nw_reset_nodes"
3862 bl_label = "Reset Nodes"
3863 bl_options = {'REGISTER', 'UNDO'}
3865 @classmethod
3866 def poll(cls, context):
3867 space = context.space_data
3868 return space.type == 'NODE_EDITOR'
3870 def execute(self, context):
3871 node_active = context.active_node
3872 node_selected = context.selected_nodes
3873 node_ignore = ["FRAME","REROUTE", "GROUP"]
3875 # Check if one node is selected at least
3876 if not (len(node_selected) > 0):
3877 self.report({'ERROR'}, "1 node must be selected at least")
3878 return {'CANCELLED'}
3880 active_node_name = node_active.name if node_active.select else None
3881 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3883 # Create output lists
3884 selected_node_names = [n.name for n in node_selected]
3885 success_names = []
3887 # Reset all valid children in a frame
3888 node_active_is_frame = False
3889 if len(node_selected) == 1 and node_active.type == "FRAME":
3890 node_tree = node_active.id_data
3891 children = [n for n in node_tree.nodes if n.parent == node_active]
3892 if children:
3893 valid_nodes = [n for n in children if n.type not in node_ignore]
3894 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3895 node_active_is_frame = True
3897 # Check if valid nodes in selection
3898 if not (len(valid_nodes) > 0):
3899 # Check for frames only
3900 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3901 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3902 self.report({'ERROR'}, "Please select only 1 frame to reset")
3903 else:
3904 self.report({'ERROR'}, "No valid node(s) in selection")
3905 return {'CANCELLED'}
3907 # Report nodes that are not valid
3908 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3909 valid_node_names = [n.name for n in valid_nodes]
3910 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3911 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3913 # Deselect all nodes
3914 for i in node_selected:
3915 i.select = False
3917 # Run through all valid nodes
3918 for node in valid_nodes:
3920 parent = node.parent if node.parent else None
3921 node_loc = [node.location.x, node.location.y]
3923 node_tree = node.id_data
3924 props_to_copy = 'bl_idname name location height width'.split(' ')
3926 reconnections = []
3927 mappings = chain.from_iterable([node.inputs, node.outputs])
3928 for i in (i for i in mappings if i.is_linked):
3929 for L in i.links:
3930 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3932 props = {j: getattr(node, j) for j in props_to_copy}
3934 new_node = node_tree.nodes.new(props['bl_idname'])
3935 props_to_copy.pop(0)
3937 for prop in props_to_copy:
3938 setattr(new_node, prop, props[prop])
3940 nodes = node_tree.nodes
3941 nodes.remove(node)
3942 new_node.name = props['name']
3944 if parent:
3945 new_node.parent = parent
3946 new_node.location = node_loc
3948 for str_from, str_to in reconnections:
3949 node_tree.links.new(eval(str_from), eval(str_to))
3951 new_node.select = False
3952 success_names.append(new_node.name)
3954 # Reselect all nodes
3955 if selected_node_names and node_active_is_frame is False:
3956 for i in selected_node_names:
3957 node_tree.nodes[i].select = True
3959 if active_node_name is not None:
3960 node_tree.nodes[active_node_name].select = True
3961 node_tree.nodes.active = node_tree.nodes[active_node_name]
3963 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3964 return {'FINISHED'}
3968 # P A N E L
3971 def drawlayout(context, layout, mode='non-panel'):
3972 tree_type = context.space_data.tree_type
3974 col = layout.column(align=True)
3975 col.menu(NWMergeNodesMenu.bl_idname)
3976 col.separator()
3978 col = layout.column(align=True)
3979 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3980 col.separator()
3982 if tree_type == 'ShaderNodeTree':
3983 col = layout.column(align=True)
3984 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3985 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3986 col.separator()
3988 col = layout.column(align=True)
3989 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3990 col.operator(NWSwapLinks.bl_idname)
3991 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3992 col.separator()
3994 col = layout.column(align=True)
3995 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3996 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3997 col.separator()
3999 col = layout.column(align=True)
4000 if mode == 'panel':
4001 row = col.row(align=True)
4002 row.operator(NWClearLabel.bl_idname).option = True
4003 row.operator(NWModifyLabels.bl_idname)
4004 else:
4005 col.operator(NWClearLabel.bl_idname).option = True
4006 col.operator(NWModifyLabels.bl_idname)
4007 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
4008 col.separator()
4009 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
4010 col.separator()
4012 col = layout.column(align=True)
4013 if tree_type == 'CompositorNodeTree':
4014 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
4015 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
4016 col.separator()
4018 col = layout.column(align=True)
4019 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
4020 col.separator()
4022 col = layout.column(align=True)
4023 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
4024 col.separator()
4026 col = layout.column(align=True)
4027 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
4028 col.separator()
4031 class NodeWranglerPanel(Panel, NWBase):
4032 bl_idname = "NODE_PT_nw_node_wrangler"
4033 bl_space_type = 'NODE_EDITOR'
4034 bl_label = "Node Wrangler"
4035 bl_region_type = "UI"
4036 bl_category = "Node Wrangler"
4038 prepend: StringProperty(
4039 name='prepend',
4041 append: StringProperty()
4042 remove: StringProperty()
4044 def draw(self, context):
4045 self.layout.label(text="(Quick access: Shift+W)")
4046 drawlayout(context, self.layout, mode='panel')
4050 # M E N U S
4052 class NodeWranglerMenu(Menu, NWBase):
4053 bl_idname = "NODE_MT_nw_node_wrangler_menu"
4054 bl_label = "Node Wrangler"
4056 def draw(self, context):
4057 self.layout.operator_context = 'INVOKE_DEFAULT'
4058 drawlayout(context, self.layout)
4061 class NWMergeNodesMenu(Menu, NWBase):
4062 bl_idname = "NODE_MT_nw_merge_nodes_menu"
4063 bl_label = "Merge Selected Nodes"
4065 def draw(self, context):
4066 type = context.space_data.tree_type
4067 layout = self.layout
4068 if type == 'ShaderNodeTree':
4069 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
4070 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
4071 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
4072 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
4073 props.mode = 'MIX'
4074 props.merge_type = 'ZCOMBINE'
4075 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
4076 props.mode = 'MIX'
4077 props.merge_type = 'ALPHAOVER'
4080 class NWMergeShadersMenu(Menu, NWBase):
4081 bl_idname = "NODE_MT_nw_merge_shaders_menu"
4082 bl_label = "Merge Selected Nodes using Shaders"
4084 def draw(self, context):
4085 layout = self.layout
4086 for type in ('MIX', 'ADD'):
4087 props = layout.operator(NWMergeNodes.bl_idname, text=type)
4088 props.mode = type
4089 props.merge_type = 'SHADER'
4092 class NWMergeMixMenu(Menu, NWBase):
4093 bl_idname = "NODE_MT_nw_merge_mix_menu"
4094 bl_label = "Merge Selected Nodes using Mix"
4096 def draw(self, context):
4097 layout = self.layout
4098 for type, name, description in blend_types:
4099 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4100 props.mode = type
4101 props.merge_type = 'MIX'
4104 class NWConnectionListOutputs(Menu, NWBase):
4105 bl_idname = "NODE_MT_nw_connection_list_out"
4106 bl_label = "From:"
4108 def draw(self, context):
4109 layout = self.layout
4110 nodes, links = get_nodes_links(context)
4112 n1 = nodes[context.scene.NWLazySource]
4114 if n1.type == "R_LAYERS":
4115 index=0
4116 for o in n1.outputs:
4117 if o.enabled: # Check which passes the render layer has enabled
4118 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
4119 index+=1
4120 else:
4121 index=0
4122 for o in n1.outputs:
4123 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
4124 index+=1
4127 class NWConnectionListInputs(Menu, NWBase):
4128 bl_idname = "NODE_MT_nw_connection_list_in"
4129 bl_label = "To:"
4131 def draw(self, context):
4132 layout = self.layout
4133 nodes, links = get_nodes_links(context)
4135 n2 = nodes[context.scene.NWLazyTarget]
4137 index = 0
4138 for i in n2.inputs:
4139 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
4140 op.from_socket = context.scene.NWSourceSocket
4141 op.to_socket = index
4142 index+=1
4145 class NWMergeMathMenu(Menu, NWBase):
4146 bl_idname = "NODE_MT_nw_merge_math_menu"
4147 bl_label = "Merge Selected Nodes using Math"
4149 def draw(self, context):
4150 layout = self.layout
4151 for type, name, description in operations:
4152 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4153 props.mode = type
4154 props.merge_type = 'MATH'
4157 class NWBatchChangeNodesMenu(Menu, NWBase):
4158 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
4159 bl_label = "Batch Change Selected Nodes"
4161 def draw(self, context):
4162 layout = self.layout
4163 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
4164 layout.menu(NWBatchChangeOperationMenu.bl_idname)
4167 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
4168 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
4169 bl_label = "Batch Change Blend Type"
4171 def draw(self, context):
4172 layout = self.layout
4173 for type, name, description in blend_types:
4174 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
4175 props.blend_type = type
4176 props.operation = 'CURRENT'
4179 class NWBatchChangeOperationMenu(Menu, NWBase):
4180 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
4181 bl_label = "Batch Change Math Operation"
4183 def draw(self, context):
4184 layout = self.layout
4185 for type, name, description in operations:
4186 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
4187 props.blend_type = 'CURRENT'
4188 props.operation = type
4191 class NWCopyToSelectedMenu(Menu, NWBase):
4192 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
4193 bl_label = "Copy to Selected"
4195 def draw(self, context):
4196 layout = self.layout
4197 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
4198 layout.menu(NWCopyLabelMenu.bl_idname)
4201 class NWCopyLabelMenu(Menu, NWBase):
4202 bl_idname = "NODE_MT_nw_copy_label_menu"
4203 bl_label = "Copy Label"
4205 def draw(self, context):
4206 layout = self.layout
4207 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
4208 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
4209 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
4212 class NWAddReroutesMenu(Menu, NWBase):
4213 bl_idname = "NODE_MT_nw_add_reroutes_menu"
4214 bl_label = "Add Reroutes"
4215 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
4217 def draw(self, context):
4218 layout = self.layout
4219 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
4220 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
4221 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
4224 class NWLinkActiveToSelectedMenu(Menu, NWBase):
4225 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
4226 bl_label = "Link Active to Selected"
4228 def draw(self, context):
4229 layout = self.layout
4230 layout.menu(NWLinkStandardMenu.bl_idname)
4231 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
4232 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
4235 class NWLinkStandardMenu(Menu, NWBase):
4236 bl_idname = "NODE_MT_nw_link_standard_menu"
4237 bl_label = "To All Selected"
4239 def draw(self, context):
4240 layout = self.layout
4241 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4242 props.replace = False
4243 props.use_node_name = False
4244 props.use_outputs_names = False
4245 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4246 props.replace = True
4247 props.use_node_name = False
4248 props.use_outputs_names = False
4251 class NWLinkUseNodeNameMenu(Menu, NWBase):
4252 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
4253 bl_label = "Use Node Name/Label"
4255 def draw(self, context):
4256 layout = self.layout
4257 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4258 props.replace = False
4259 props.use_node_name = True
4260 props.use_outputs_names = False
4261 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4262 props.replace = True
4263 props.use_node_name = True
4264 props.use_outputs_names = False
4267 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
4268 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
4269 bl_label = "Use Outputs Names"
4271 def draw(self, context):
4272 layout = self.layout
4273 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4274 props.replace = False
4275 props.use_node_name = False
4276 props.use_outputs_names = True
4277 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4278 props.replace = True
4279 props.use_node_name = False
4280 props.use_outputs_names = True
4283 class NWVertColMenu(bpy.types.Menu):
4284 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
4285 bl_label = "Vertex Colors"
4287 @classmethod
4288 def poll(cls, context):
4289 valid = False
4290 if nw_check(context):
4291 snode = context.space_data
4292 valid = snode.tree_type == 'ShaderNodeTree'
4293 return valid
4295 def draw(self, context):
4296 l = self.layout
4297 nodes, links = get_nodes_links(context)
4298 mat = context.object.active_material
4300 objs = []
4301 for obj in bpy.data.objects:
4302 for slot in obj.material_slots:
4303 if slot.material == mat:
4304 objs.append(obj)
4305 vcols = []
4306 for obj in objs:
4307 if obj.data.vertex_colors:
4308 for vcol in obj.data.vertex_colors:
4309 vcols.append(vcol.name)
4310 vcols = list(set(vcols)) # get a unique list
4312 if vcols:
4313 for vcol in vcols:
4314 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
4315 else:
4316 l.label(text="No Vertex Color layers on objects with this material")
4319 class NWSwitchNodeTypeMenu(Menu, NWBase):
4320 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4321 bl_label = "Switch Type to..."
4323 def draw(self, context):
4324 layout = self.layout
4325 tree = context.space_data.node_tree
4326 if tree.type == 'SHADER':
4327 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
4328 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
4329 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
4330 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
4331 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
4332 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
4333 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
4334 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
4335 if tree.type == 'COMPOSITING':
4336 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
4337 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
4338 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
4339 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
4340 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
4341 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
4342 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
4343 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
4344 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
4345 if tree.type == 'TEXTURE':
4346 layout.menu(NWSwitchTexInputSubmenu.bl_idname)
4347 layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
4348 layout.menu(NWSwitchTexColorSubmenu.bl_idname)
4349 layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
4350 layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
4351 layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
4352 layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
4353 layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
4356 class NWSwitchShadersInputSubmenu(Menu, NWBase):
4357 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
4358 bl_label = "Input"
4360 def draw(self, context):
4361 layout = self.layout
4362 for ident, node_type, rna_name in shaders_input_nodes_props:
4363 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4364 props.to_type = ident
4367 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
4368 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
4369 bl_label = "Output"
4371 def draw(self, context):
4372 layout = self.layout
4373 for ident, node_type, rna_name in shaders_output_nodes_props:
4374 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4375 props.to_type = ident
4378 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
4379 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
4380 bl_label = "Shader"
4382 def draw(self, context):
4383 layout = self.layout
4384 for ident, node_type, rna_name in shaders_shader_nodes_props:
4385 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4386 props.to_type = ident
4389 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
4390 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
4391 bl_label = "Texture"
4393 def draw(self, context):
4394 layout = self.layout
4395 for ident, node_type, rna_name in shaders_texture_nodes_props:
4396 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4397 props.to_type = ident
4400 class NWSwitchShadersColorSubmenu(Menu, NWBase):
4401 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
4402 bl_label = "Color"
4404 def draw(self, context):
4405 layout = self.layout
4406 for ident, node_type, rna_name in shaders_color_nodes_props:
4407 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4408 props.to_type = ident
4411 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
4412 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
4413 bl_label = "Vector"
4415 def draw(self, context):
4416 layout = self.layout
4417 for ident, node_type, rna_name in shaders_vector_nodes_props:
4418 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4419 props.to_type = ident
4422 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
4423 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
4424 bl_label = "Converter"
4426 def draw(self, context):
4427 layout = self.layout
4428 for ident, node_type, rna_name in shaders_converter_nodes_props:
4429 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4430 props.to_type = ident
4433 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
4434 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
4435 bl_label = "Layout"
4437 def draw(self, context):
4438 layout = self.layout
4439 for ident, node_type, rna_name in shaders_layout_nodes_props:
4440 if node_type != 'FRAME':
4441 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4442 props.to_type = ident
4445 class NWSwitchCompoInputSubmenu(Menu, NWBase):
4446 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
4447 bl_label = "Input"
4449 def draw(self, context):
4450 layout = self.layout
4451 for ident, node_type, rna_name in compo_input_nodes_props:
4452 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4453 props.to_type = ident
4456 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
4457 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
4458 bl_label = "Output"
4460 def draw(self, context):
4461 layout = self.layout
4462 for ident, node_type, rna_name in compo_output_nodes_props:
4463 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4464 props.to_type = ident
4467 class NWSwitchCompoColorSubmenu(Menu, NWBase):
4468 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
4469 bl_label = "Color"
4471 def draw(self, context):
4472 layout = self.layout
4473 for ident, node_type, rna_name in compo_color_nodes_props:
4474 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4475 props.to_type = ident
4478 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
4479 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
4480 bl_label = "Converter"
4482 def draw(self, context):
4483 layout = self.layout
4484 for ident, node_type, rna_name in compo_converter_nodes_props:
4485 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4486 props.to_type = ident
4489 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
4490 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
4491 bl_label = "Filter"
4493 def draw(self, context):
4494 layout = self.layout
4495 for ident, node_type, rna_name in compo_filter_nodes_props:
4496 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4497 props.to_type = ident
4500 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
4501 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
4502 bl_label = "Vector"
4504 def draw(self, context):
4505 layout = self.layout
4506 for ident, node_type, rna_name in compo_vector_nodes_props:
4507 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4508 props.to_type = ident
4511 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
4512 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
4513 bl_label = "Matte"
4515 def draw(self, context):
4516 layout = self.layout
4517 for ident, node_type, rna_name in compo_matte_nodes_props:
4518 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4519 props.to_type = ident
4522 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
4523 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
4524 bl_label = "Distort"
4526 def draw(self, context):
4527 layout = self.layout
4528 for ident, node_type, rna_name in compo_distort_nodes_props:
4529 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4530 props.to_type = ident
4533 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
4534 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
4535 bl_label = "Layout"
4537 def draw(self, context):
4538 layout = self.layout
4539 for ident, node_type, rna_name in compo_layout_nodes_props:
4540 if node_type != 'FRAME':
4541 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4542 props.to_type = ident
4545 class NWSwitchMatInputSubmenu(Menu, NWBase):
4546 bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
4547 bl_label = "Input"
4549 def draw(self, context):
4550 layout = self.layout
4551 for ident, node_type, rna_name in sorted(blender_mat_input_nodes_props, key=lambda k: k[2]):
4552 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4553 props.to_type = ident
4556 class NWSwitchMatOutputSubmenu(Menu, NWBase):
4557 bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
4558 bl_label = "Output"
4560 def draw(self, context):
4561 layout = self.layout
4562 for ident, node_type, rna_name in sorted(blender_mat_output_nodes_props, key=lambda k: k[2]):
4563 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4564 props.to_type = ident
4567 class NWSwitchMatColorSubmenu(Menu, NWBase):
4568 bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
4569 bl_label = "Color"
4571 def draw(self, context):
4572 layout = self.layout
4573 for ident, node_type, rna_name in sorted(blender_mat_color_nodes_props, key=lambda k: k[2]):
4574 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4575 props.to_type = ident
4578 class NWSwitchMatVectorSubmenu(Menu, NWBase):
4579 bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
4580 bl_label = "Vector"
4582 def draw(self, context):
4583 layout = self.layout
4584 for ident, node_type, rna_name in sorted(blender_mat_vector_nodes_props, key=lambda k: k[2]):
4585 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4586 props.to_type = ident
4589 class NWSwitchMatConverterSubmenu(Menu, NWBase):
4590 bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
4591 bl_label = "Converter"
4593 def draw(self, context):
4594 layout = self.layout
4595 for ident, node_type, rna_name in sorted(blender_mat_converter_nodes_props, key=lambda k: k[2]):
4596 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4597 props.to_type = ident
4600 class NWSwitchMatLayoutSubmenu(Menu, NWBase):
4601 bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
4602 bl_label = "Layout"
4604 def draw(self, context):
4605 layout = self.layout
4606 for ident, node_type, rna_name in sorted(blender_mat_layout_nodes_props, key=lambda k: k[2]):
4607 if node_type != 'FRAME':
4608 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4609 props.to_type = ident
4612 class NWSwitchTexInputSubmenu(Menu, NWBase):
4613 bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
4614 bl_label = "Input"
4616 def draw(self, context):
4617 layout = self.layout
4618 for ident, node_type, rna_name in sorted(texture_input_nodes_props, key=lambda k: k[2]):
4619 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4620 props.to_type = ident
4623 class NWSwitchTexOutputSubmenu(Menu, NWBase):
4624 bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
4625 bl_label = "Output"
4627 def draw(self, context):
4628 layout = self.layout
4629 for ident, node_type, rna_name in sorted(texture_output_nodes_props, key=lambda k: k[2]):
4630 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4631 props.to_type = ident
4634 class NWSwitchTexColorSubmenu(Menu, NWBase):
4635 bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
4636 bl_label = "Color"
4638 def draw(self, context):
4639 layout = self.layout
4640 for ident, node_type, rna_name in sorted(texture_color_nodes_props, key=lambda k: k[2]):
4641 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4642 props.to_type = ident
4645 class NWSwitchTexPatternSubmenu(Menu, NWBase):
4646 bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
4647 bl_label = "Pattern"
4649 def draw(self, context):
4650 layout = self.layout
4651 for ident, node_type, rna_name in sorted(texture_pattern_nodes_props, key=lambda k: k[2]):
4652 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4653 props.to_type = ident
4656 class NWSwitchTexTexturesSubmenu(Menu, NWBase):
4657 bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
4658 bl_label = "Textures"
4660 def draw(self, context):
4661 layout = self.layout
4662 for ident, node_type, rna_name in sorted(texture_textures_nodes_props, key=lambda k: k[2]):
4663 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4664 props.to_type = ident
4667 class NWSwitchTexConverterSubmenu(Menu, NWBase):
4668 bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
4669 bl_label = "Converter"
4671 def draw(self, context):
4672 layout = self.layout
4673 for ident, node_type, rna_name in sorted(texture_converter_nodes_props, key=lambda k: k[2]):
4674 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4675 props.to_type = ident
4678 class NWSwitchTexDistortSubmenu(Menu, NWBase):
4679 bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
4680 bl_label = "Distort"
4682 def draw(self, context):
4683 layout = self.layout
4684 for ident, node_type, rna_name in sorted(texture_distort_nodes_props, key=lambda k: k[2]):
4685 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4686 props.to_type = ident
4689 class NWSwitchTexLayoutSubmenu(Menu, NWBase):
4690 bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
4691 bl_label = "Layout"
4693 def draw(self, context):
4694 layout = self.layout
4695 for ident, node_type, rna_name in sorted(texture_layout_nodes_props, key=lambda k: k[2]):
4696 if node_type != 'FRAME':
4697 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4698 props.to_type = ident
4702 # APPENDAGES TO EXISTING UI
4706 def select_parent_children_buttons(self, context):
4707 layout = self.layout
4708 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4709 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4712 def attr_nodes_menu_func(self, context):
4713 col = self.layout.column(align=True)
4714 col.menu("NODE_MT_nw_node_vertex_color_menu")
4715 col.separator()
4718 def multipleimages_menu_func(self, context):
4719 col = self.layout.column(align=True)
4720 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4721 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4722 col.separator()
4725 def bgreset_menu_func(self, context):
4726 self.layout.operator(NWResetBG.bl_idname)
4729 def save_viewer_menu_func(self, context):
4730 if nw_check(context):
4731 if context.space_data.tree_type == 'CompositorNodeTree':
4732 if context.scene.node_tree.nodes.active:
4733 if context.scene.node_tree.nodes.active.type == "VIEWER":
4734 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4737 def reset_nodes_button(self, context):
4738 node_active = context.active_node
4739 node_selected = context.selected_nodes
4740 node_ignore = ["FRAME","REROUTE", "GROUP"]
4742 # Check if active node is in the selection and respective type
4743 if (len(node_selected) == 1) and node_active.select and node_active.type not in node_ignore:
4744 row = self.layout.row()
4745 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4746 self.layout.separator()
4748 elif (len(node_selected) == 1) and node_active.select and node_active.type == "FRAME":
4749 row = self.layout.row()
4750 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4751 self.layout.separator()
4755 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4758 addon_keymaps = []
4759 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4760 # props entry: (property name, property value)
4761 kmi_defs = (
4762 # MERGE NODES
4763 # NWMergeNodes with Ctrl (AUTO).
4764 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4765 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4766 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4767 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4768 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4769 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4770 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4771 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4772 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4773 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4774 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4775 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4776 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4777 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4778 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4779 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4780 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4781 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4782 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4783 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4784 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4785 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4786 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4787 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4788 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4789 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4790 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4791 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4792 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4793 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4794 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4795 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4796 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4797 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4798 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4799 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4800 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4801 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4802 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4803 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4804 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4805 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4806 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4807 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4808 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4809 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4810 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4811 # NWMergeNodes with Ctrl Shift (MATH)
4812 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4813 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4814 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4815 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4816 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4817 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4818 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4819 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4820 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4821 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4822 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4823 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4824 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4825 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4826 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4827 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4828 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4829 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4830 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4831 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4832 # BATCH CHANGE NODES
4833 # NWBatchChangeNodes with Alt
4834 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4835 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4836 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4837 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4838 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4839 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4840 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4841 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4842 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4843 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4844 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4845 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4846 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4847 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4848 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4849 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4850 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4851 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4852 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4853 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4854 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4855 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4856 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4857 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4858 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4859 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4860 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4861 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4862 # LINK ACTIVE TO SELECTED
4863 # Don't use names, don't replace links (K)
4864 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4865 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4866 # Don't use names, replace links (Shift K)
4867 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4868 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4869 # Use node name, don't replace links (')
4870 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4871 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4872 # Use node name, replace links (Shift ')
4873 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4874 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4875 # Don't use names, don't replace links (;)
4876 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4877 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4878 # Don't use names, replace links (')
4879 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4880 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4881 # CHANGE MIX FACTOR
4882 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4883 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4884 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4885 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4886 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4887 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4888 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4889 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4890 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4891 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4892 # CLEAR LABEL (Alt L)
4893 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4894 # MODIFY LABEL (Alt Shift L)
4895 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4896 # Copy Label from active to selected
4897 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4898 # DETACH OUTPUTS (Alt Shift D)
4899 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4900 # LINK TO OUTPUT NODE (O)
4901 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4902 # SELECT PARENT/CHILDREN
4903 # Select Children
4904 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4905 # Select Parent
4906 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4907 # Add Texture Setup
4908 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4909 # Add Principled BSDF Texture Setup
4910 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4911 # Reset backdrop
4912 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4913 # Delete unused
4914 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4915 # Frame Selected
4916 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4917 # Swap Outputs
4918 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
4919 # Emission Viewer
4920 (NWEmissionViewer.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, None, "Connect to Cycles Viewer node"),
4921 # Reload Images
4922 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4923 # Lazy Mix
4924 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4925 # Lazy Connect
4926 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
4927 # Lazy Connect with Menu
4928 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4929 # Viewer Tile Center
4930 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4931 # Align Nodes
4932 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4933 # Reset Nodes (Back Space)
4934 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4935 # MENUS
4936 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
4937 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4938 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4939 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4940 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4941 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4945 classes = (
4946 NWPrincipledPreferences,
4947 NWNodeWrangler,
4948 NWLazyMix,
4949 NWLazyConnect,
4950 NWDeleteUnused,
4951 NWSwapLinks,
4952 NWResetBG,
4953 NWAddAttrNode,
4954 NWEmissionViewer,
4955 NWFrameSelected,
4956 NWReloadImages,
4957 NWSwitchNodeType,
4958 NWMergeNodes,
4959 NWBatchChangeNodes,
4960 NWChangeMixFactor,
4961 NWCopySettings,
4962 NWCopyLabel,
4963 NWClearLabel,
4964 NWModifyLabels,
4965 NWAddTextureSetup,
4966 NWAddPrincipledSetup,
4967 NWAddReroutes,
4968 NWLinkActiveToSelected,
4969 NWAlignNodes,
4970 NWSelectParentChildren,
4971 NWDetachOutputs,
4972 NWLinkToOutputNode,
4973 NWMakeLink,
4974 NWCallInputsMenu,
4975 NWAddSequence,
4976 NWAddMultipleImages,
4977 NWViewerFocus,
4978 NWSaveViewer,
4979 NWResetNodes,
4980 NodeWranglerPanel,
4981 NodeWranglerMenu,
4982 NWMergeNodesMenu,
4983 NWMergeShadersMenu,
4984 NWMergeMixMenu,
4985 NWConnectionListOutputs,
4986 NWConnectionListInputs,
4987 NWMergeMathMenu,
4988 NWBatchChangeNodesMenu,
4989 NWBatchChangeBlendTypeMenu,
4990 NWBatchChangeOperationMenu,
4991 NWCopyToSelectedMenu,
4992 NWCopyLabelMenu,
4993 NWAddReroutesMenu,
4994 NWLinkActiveToSelectedMenu,
4995 NWLinkStandardMenu,
4996 NWLinkUseNodeNameMenu,
4997 NWLinkUseOutputsNamesMenu,
4998 NWVertColMenu,
4999 NWSwitchNodeTypeMenu,
5000 NWSwitchShadersInputSubmenu,
5001 NWSwitchShadersOutputSubmenu,
5002 NWSwitchShadersShaderSubmenu,
5003 NWSwitchShadersTextureSubmenu,
5004 NWSwitchShadersColorSubmenu,
5005 NWSwitchShadersVectorSubmenu,
5006 NWSwitchShadersConverterSubmenu,
5007 NWSwitchShadersLayoutSubmenu,
5008 NWSwitchCompoInputSubmenu,
5009 NWSwitchCompoOutputSubmenu,
5010 NWSwitchCompoColorSubmenu,
5011 NWSwitchCompoConverterSubmenu,
5012 NWSwitchCompoFilterSubmenu,
5013 NWSwitchCompoVectorSubmenu,
5014 NWSwitchCompoMatteSubmenu,
5015 NWSwitchCompoDistortSubmenu,
5016 NWSwitchCompoLayoutSubmenu,
5017 NWSwitchMatInputSubmenu,
5018 NWSwitchMatOutputSubmenu,
5019 NWSwitchMatColorSubmenu,
5020 NWSwitchMatVectorSubmenu,
5021 NWSwitchMatConverterSubmenu,
5022 NWSwitchMatLayoutSubmenu,
5023 NWSwitchTexInputSubmenu,
5024 NWSwitchTexOutputSubmenu,
5025 NWSwitchTexColorSubmenu,
5026 NWSwitchTexPatternSubmenu,
5027 NWSwitchTexTexturesSubmenu,
5028 NWSwitchTexConverterSubmenu,
5029 NWSwitchTexDistortSubmenu,
5030 NWSwitchTexLayoutSubmenu,
5033 def register():
5034 from bpy.utils import register_class
5036 # props
5037 bpy.types.Scene.NWBusyDrawing = StringProperty(
5038 name="Busy Drawing!",
5039 default="",
5040 description="An internal property used to store only the first mouse position")
5041 bpy.types.Scene.NWLazySource = StringProperty(
5042 name="Lazy Source!",
5043 default="x",
5044 description="An internal property used to store the first node in a Lazy Connect operation")
5045 bpy.types.Scene.NWLazyTarget = StringProperty(
5046 name="Lazy Target!",
5047 default="x",
5048 description="An internal property used to store the last node in a Lazy Connect operation")
5049 bpy.types.Scene.NWSourceSocket = IntProperty(
5050 name="Source Socket!",
5051 default=0,
5052 description="An internal property used to store the source socket in a Lazy Connect operation")
5053 bpy.types.NodeSocketInterface.NWViewerSocket = BoolProperty(
5054 name="NW Socket",
5055 default=False,
5056 description="An internal property used to determine if a socket is generated by the addon"
5059 for cls in classes:
5060 register_class(cls)
5062 # keymaps
5063 addon_keymaps.clear()
5064 kc = bpy.context.window_manager.keyconfigs.addon
5065 if kc:
5066 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
5067 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
5068 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
5069 if props:
5070 for prop, value in props:
5071 setattr(kmi.properties, prop, value)
5072 addon_keymaps.append((km, kmi))
5074 # menu items
5075 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
5076 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
5077 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
5078 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
5079 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
5080 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
5081 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
5082 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
5085 def unregister():
5086 from bpy.utils import unregister_class
5088 # props
5089 del bpy.types.Scene.NWBusyDrawing
5090 del bpy.types.Scene.NWLazySource
5091 del bpy.types.Scene.NWLazyTarget
5092 del bpy.types.Scene.NWSourceSocket
5093 del bpy.types.NodeSocketInterface.NWViewerSocket
5095 # keymaps
5096 for km, kmi in addon_keymaps:
5097 km.keymap_items.remove(kmi)
5098 addon_keymaps.clear()
5100 # menuitems
5101 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
5102 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
5103 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
5104 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
5105 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
5106 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
5107 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
5108 bpy.types.NODE_MT_node.remove(reset_nodes_button)
5110 for cls in classes:
5111 unregister_class(cls)
5113 if __name__ == "__main__":
5114 register()