Cleanup: trailing space
[blender-addons.git] / node_wrangler.py
blob05af47d6216ec57bf1360f869996725d11c4b90e
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
6 "version": (3, 38),
7 "blender": (2, 93, 0),
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
10 "warning": "",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
12 "category": "Node",
15 import bpy, blf, bgl
16 import gpu
17 from bpy.types import Operator, Panel, Menu
18 from bpy.props import (
19 FloatProperty,
20 EnumProperty,
21 BoolProperty,
22 IntProperty,
23 StringProperty,
24 FloatVectorProperty,
25 CollectionProperty,
27 from bpy_extras.io_utils import ImportHelper, ExportHelper
28 from gpu_extras.batch import batch_for_shader
29 from mathutils import Vector
30 from nodeitems_utils import node_categories_iter, NodeItemCustom
31 from math import cos, sin, pi, hypot
32 from os import path
33 from glob import glob
34 from copy import copy
35 from itertools import chain
36 import re
37 from collections import namedtuple
39 #################
40 # rl_outputs:
41 # list of outputs of Input Render Layer
42 # with attributes determining if pass is used,
43 # and MultiLayer EXR outputs names and corresponding render engines
45 # rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
46 RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
47 rl_outputs = (
48 RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
49 RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
50 RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
51 RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
52 RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
53 RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
54 RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
55 RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
56 RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
57 RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
58 RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
59 RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
60 RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
61 RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
62 RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
63 RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
64 RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
65 RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
66 RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
67 RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
68 RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
69 RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
70 RL_entry('use_pass_uv', 'UV', 'UV', True, True),
71 RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
72 RL_entry('use_pass_z', 'Z', 'Depth', True, True),
75 # shader nodes
76 # (rna_type.identifier, type, rna_type.name)
77 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
78 # Keeping things in alphabetical order so we don't need to sort later.
79 shaders_input_nodes_props = (
80 ('ShaderNodeAmbientOcclusion', 'AMBIENT_OCCLUSION', 'Ambient Occlusion'),
81 ('ShaderNodeAttribute', 'ATTRIBUTE', 'Attribute'),
82 ('ShaderNodeBevel', 'BEVEL', 'Bevel'),
83 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
84 ('ShaderNodeFresnel', 'FRESNEL', 'Fresnel'),
85 ('ShaderNodeNewGeometry', 'NEW_GEOMETRY', 'Geometry'),
86 ('ShaderNodeHairInfo', 'HAIR_INFO', 'Hair Info'),
87 ('ShaderNodeLayerWeight', 'LAYER_WEIGHT', 'Layer Weight'),
88 ('ShaderNodeLightPath', 'LIGHT_PATH', 'Light Path'),
89 ('ShaderNodeObjectInfo', 'OBJECT_INFO', 'Object Info'),
90 ('ShaderNodeParticleInfo', 'PARTICLE_INFO', 'Particle Info'),
91 ('ShaderNodeRGB', 'RGB', 'RGB'),
92 ('ShaderNodeTangent', 'TANGENT', 'Tangent'),
93 ('ShaderNodeTexCoord', 'TEX_COORD', 'Texture Coordinate'),
94 ('ShaderNodeUVMap', 'UVMAP', 'UV Map'),
95 ('ShaderNodeValue', 'VALUE', 'Value'),
96 ('ShaderNodeVertexColor', 'VERTEX_COLOR', 'Vertex Color'),
97 ('ShaderNodeVolumeInfo', 'VOLUME_INFO', 'Volume Info'),
98 ('ShaderNodeWireframe', 'WIREFRAME', 'Wireframe'),
101 # (rna_type.identifier, type, rna_type.name)
102 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
103 # Keeping things in alphabetical order so we don't need to sort later.
104 shaders_output_nodes_props = (
105 ('ShaderNodeOutputAOV', 'OUTPUT_AOV', 'AOV Output'),
106 ('ShaderNodeOutputLight', 'OUTPUT_LIGHT', 'Light Output'),
107 ('ShaderNodeOutputMaterial', 'OUTPUT_MATERIAL', 'Material Output'),
108 ('ShaderNodeOutputWorld', 'OUTPUT_WORLD', 'World Output'),
110 # (rna_type.identifier, type, rna_type.name)
111 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
112 # Keeping things in alphabetical order so we don't need to sort later.
113 shaders_shader_nodes_props = (
114 ('ShaderNodeAddShader', 'ADD_SHADER', 'Add Shader'),
115 ('ShaderNodeBsdfAnisotropic', 'BSDF_ANISOTROPIC', 'Anisotropic BSDF'),
116 ('ShaderNodeBsdfDiffuse', 'BSDF_DIFFUSE', 'Diffuse BSDF'),
117 ('ShaderNodeEmission', 'EMISSION', 'Emission'),
118 ('ShaderNodeBsdfGlass', 'BSDF_GLASS', 'Glass BSDF'),
119 ('ShaderNodeBsdfGlossy', 'BSDF_GLOSSY', 'Glossy BSDF'),
120 ('ShaderNodeBsdfHair', 'BSDF_HAIR', 'Hair BSDF'),
121 ('ShaderNodeHoldout', 'HOLDOUT', 'Holdout'),
122 ('ShaderNodeMixShader', 'MIX_SHADER', 'Mix Shader'),
123 ('ShaderNodeBsdfPrincipled', 'BSDF_PRINCIPLED', 'Principled BSDF'),
124 ('ShaderNodeBsdfHairPrincipled', 'BSDF_HAIR_PRINCIPLED', 'Principled Hair BSDF'),
125 ('ShaderNodeVolumePrincipled', 'PRINCIPLED_VOLUME', 'Principled Volume'),
126 ('ShaderNodeBsdfRefraction', 'BSDF_REFRACTION', 'Refraction BSDF'),
127 ('ShaderNodeSubsurfaceScattering', 'SUBSURFACE_SCATTERING', 'Subsurface Scattering'),
128 ('ShaderNodeBsdfToon', 'BSDF_TOON', 'Toon BSDF'),
129 ('ShaderNodeBsdfTranslucent', 'BSDF_TRANSLUCENT', 'Translucent BSDF'),
130 ('ShaderNodeBsdfTransparent', 'BSDF_TRANSPARENT', 'Transparent BSDF'),
131 ('ShaderNodeBsdfVelvet', 'BSDF_VELVET', 'Velvet BSDF'),
132 ('ShaderNodeBackground', 'BACKGROUND', 'Background'),
133 ('ShaderNodeVolumeAbsorption', 'VOLUME_ABSORPTION', 'Volume Absorption'),
134 ('ShaderNodeVolumeScatter', 'VOLUME_SCATTER', 'Volume Scatter'),
136 # (rna_type.identifier, type, rna_type.name)
137 # Keeping things in alphabetical order so we don't need to sort later.
138 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
139 shaders_texture_nodes_props = (
140 ('ShaderNodeTexBrick', 'TEX_BRICK', 'Brick Texture'),
141 ('ShaderNodeTexChecker', 'TEX_CHECKER', 'Checker Texture'),
142 ('ShaderNodeTexEnvironment', 'TEX_ENVIRONMENT', 'Environment Texture'),
143 ('ShaderNodeTexGradient', 'TEX_GRADIENT', 'Gradient Texture'),
144 ('ShaderNodeTexIES', 'TEX_IES', 'IES Texture'),
145 ('ShaderNodeTexImage', 'TEX_IMAGE', 'Image Texture'),
146 ('ShaderNodeTexMagic', 'TEX_MAGIC', 'Magic Texture'),
147 ('ShaderNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave Texture'),
148 ('ShaderNodeTexNoise', 'TEX_NOISE', 'Noise Texture'),
149 ('ShaderNodeTexPointDensity', 'TEX_POINTDENSITY', 'Point Density'),
150 ('ShaderNodeTexSky', 'TEX_SKY', 'Sky Texture'),
151 ('ShaderNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi Texture'),
152 ('ShaderNodeTexWave', 'TEX_WAVE', 'Wave Texture'),
153 ('ShaderNodeTexWhiteNoise', 'TEX_WHITE_NOISE', 'White Noise'),
155 # (rna_type.identifier, type, rna_type.name)
156 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
157 # Keeping things in alphabetical order so we don't need to sort later.
158 shaders_color_nodes_props = (
159 ('ShaderNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright Contrast'),
160 ('ShaderNodeGamma', 'GAMMA', 'Gamma'),
161 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
162 ('ShaderNodeInvert', 'INVERT', 'Invert'),
163 ('ShaderNodeLightFalloff', 'LIGHT_FALLOFF', 'Light Falloff'),
164 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
165 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
167 # (rna_type.identifier, type, rna_type.name)
168 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
169 # Keeping things in alphabetical order so we don't need to sort later.
170 shaders_vector_nodes_props = (
171 ('ShaderNodeBump', 'BUMP', 'Bump'),
172 ('ShaderNodeDisplacement', 'DISPLACEMENT', 'Displacement'),
173 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
174 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
175 ('ShaderNodeNormalMap', 'NORMAL_MAP', 'Normal Map'),
176 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
177 ('ShaderNodeVectorDisplacement', 'VECTOR_DISPLACEMENT', 'Vector Displacement'),
178 ('ShaderNodeVectorTransform', 'VECT_TRANSFORM', 'Vector Transform'),
180 # (rna_type.identifier, type, rna_type.name)
181 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
182 # Keeping things in alphabetical order so we don't need to sort later.
183 shaders_converter_nodes_props = (
184 ('ShaderNodeBlackbody', 'BLACKBODY', 'Blackbody'),
185 ('ShaderNodeClamp', 'CLAMP', 'Clamp'),
186 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
187 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
188 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
189 ('ShaderNodeCombineXYZ', 'COMBXYZ', 'Combine XYZ'),
190 ('ShaderNodeMapRange', 'MAP_RANGE', 'Map Range'),
191 ('ShaderNodeMath', 'MATH', 'Math'),
192 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
193 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
194 ('ShaderNodeSeparateXYZ', 'SEPXYZ', 'Separate XYZ'),
195 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
196 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
197 ('ShaderNodeWavelength', 'WAVELENGTH', 'Wavelength'),
199 # (rna_type.identifier, type, rna_type.name)
200 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
201 # Keeping things in alphabetical order so we don't need to sort later.
202 shaders_layout_nodes_props = (
203 ('NodeFrame', 'FRAME', 'Frame'),
204 ('NodeReroute', 'REROUTE', 'Reroute'),
207 # compositing nodes
208 # (rna_type.identifier, type, rna_type.name)
209 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
210 # Keeping things in alphabetical order so we don't need to sort later.
211 compo_input_nodes_props = (
212 ('CompositorNodeBokehImage', 'BOKEHIMAGE', 'Bokeh Image'),
213 ('CompositorNodeImage', 'IMAGE', 'Image'),
214 ('CompositorNodeMask', 'MASK', 'Mask'),
215 ('CompositorNodeMovieClip', 'MOVIECLIP', 'Movie Clip'),
216 ('CompositorNodeRLayers', 'R_LAYERS', 'Render Layers'),
217 ('CompositorNodeRGB', 'RGB', 'RGB'),
218 ('CompositorNodeTexture', 'TEXTURE', 'Texture'),
219 ('CompositorNodeTime', 'TIME', 'Time'),
220 ('CompositorNodeTrackPos', 'TRACKPOS', 'Track Position'),
221 ('CompositorNodeValue', 'VALUE', 'Value'),
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 order so we don't need to sort later.
226 compo_output_nodes_props = (
227 ('CompositorNodeComposite', 'COMPOSITE', 'Composite'),
228 ('CompositorNodeOutputFile', 'OUTPUT_FILE', 'File Output'),
229 ('CompositorNodeLevels', 'LEVELS', 'Levels'),
230 ('CompositorNodeSplitViewer', 'SPLITVIEWER', 'Split Viewer'),
231 ('CompositorNodeViewer', 'VIEWER', 'Viewer'),
233 # (rna_type.identifier, type, rna_type.name)
234 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
235 # Keeping things in alphabetical order so we don't need to sort later.
236 compo_color_nodes_props = (
237 ('CompositorNodeAlphaOver', 'ALPHAOVER', 'Alpha Over'),
238 ('CompositorNodeBrightContrast', 'BRIGHTCONTRAST', 'Bright/Contrast'),
239 ('CompositorNodeColorBalance', 'COLORBALANCE', 'Color Balance'),
240 ('CompositorNodeColorCorrection', 'COLORCORRECTION', 'Color Correction'),
241 ('CompositorNodeGamma', 'GAMMA', 'Gamma'),
242 ('CompositorNodeHueCorrect', 'HUECORRECT', 'Hue Correct'),
243 ('CompositorNodeHueSat', 'HUE_SAT', 'Hue Saturation Value'),
244 ('CompositorNodeInvert', 'INVERT', 'Invert'),
245 ('CompositorNodeMixRGB', 'MIX_RGB', 'Mix'),
246 ('CompositorNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
247 ('CompositorNodeTonemap', 'TONEMAP', 'Tonemap'),
248 ('CompositorNodeZcombine', 'ZCOMBINE', 'Z Combine'),
250 # (rna_type.identifier, type, rna_type.name)
251 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
252 # Keeping things in alphabetical order so we don't need to sort later.
253 compo_converter_nodes_props = (
254 ('CompositorNodePremulKey', 'PREMULKEY', 'Alpha Convert'),
255 ('CompositorNodeValToRGB', 'VALTORGB', 'ColorRamp'),
256 ('CompositorNodeCombHSVA', 'COMBHSVA', 'Combine HSVA'),
257 ('CompositorNodeCombRGBA', 'COMBRGBA', 'Combine RGBA'),
258 ('CompositorNodeCombYCCA', 'COMBYCCA', 'Combine YCbCrA'),
259 ('CompositorNodeCombYUVA', 'COMBYUVA', 'Combine YUVA'),
260 ('CompositorNodeIDMask', 'ID_MASK', 'ID Mask'),
261 ('CompositorNodeMath', 'MATH', 'Math'),
262 ('CompositorNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
263 ('CompositorNodeSepRGBA', 'SEPRGBA', 'Separate RGBA'),
264 ('CompositorNodeSepHSVA', 'SEPHSVA', 'Separate HSVA'),
265 ('CompositorNodeSepYUVA', 'SEPYUVA', 'Separate YUVA'),
266 ('CompositorNodeSepYCCA', 'SEPYCCA', 'Separate YCbCrA'),
267 ('CompositorNodeSetAlpha', 'SETALPHA', 'Set Alpha'),
268 ('CompositorNodeSwitchView', 'VIEWSWITCH', 'View Switch'),
270 # (rna_type.identifier, type, rna_type.name)
271 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
272 # Keeping things in alphabetical order so we don't need to sort later.
273 compo_filter_nodes_props = (
274 ('CompositorNodeBilateralblur', 'BILATERALBLUR', 'Bilateral Blur'),
275 ('CompositorNodeBlur', 'BLUR', 'Blur'),
276 ('CompositorNodeBokehBlur', 'BOKEHBLUR', 'Bokeh Blur'),
277 ('CompositorNodeDefocus', 'DEFOCUS', 'Defocus'),
278 ('CompositorNodeDenoise', 'DENOISE', 'Denoise'),
279 ('CompositorNodeDespeckle', 'DESPECKLE', 'Despeckle'),
280 ('CompositorNodeDilateErode', 'DILATEERODE', 'Dilate/Erode'),
281 ('CompositorNodeDBlur', 'DBLUR', 'Directional Blur'),
282 ('CompositorNodeFilter', 'FILTER', 'Filter'),
283 ('CompositorNodeGlare', 'GLARE', 'Glare'),
284 ('CompositorNodeInpaint', 'INPAINT', 'Inpaint'),
285 ('CompositorNodePixelate', 'PIXELATE', 'Pixelate'),
286 ('CompositorNodeSunBeams', 'SUNBEAMS', 'Sun Beams'),
287 ('CompositorNodeVecBlur', 'VECBLUR', 'Vector Blur'),
289 # (rna_type.identifier, type, rna_type.name)
290 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
291 # Keeping things in alphabetical order so we don't need to sort later.
292 compo_vector_nodes_props = (
293 ('CompositorNodeMapRange', 'MAP_RANGE', 'Map Range'),
294 ('CompositorNodeMapValue', 'MAP_VALUE', 'Map Value'),
295 ('CompositorNodeNormal', 'NORMAL', 'Normal'),
296 ('CompositorNodeNormalize', 'NORMALIZE', 'Normalize'),
297 ('CompositorNodeCurveVec', 'CURVE_VEC', 'Vector Curves'),
299 # (rna_type.identifier, type, rna_type.name)
300 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
301 # Keeping things in alphabetical order so we don't need to sort later.
302 compo_matte_nodes_props = (
303 ('CompositorNodeBoxMask', 'BOXMASK', 'Box Mask'),
304 ('CompositorNodeChannelMatte', 'CHANNEL_MATTE', 'Channel Key'),
305 ('CompositorNodeChromaMatte', 'CHROMA_MATTE', 'Chroma Key'),
306 ('CompositorNodeColorMatte', 'COLOR_MATTE', 'Color Key'),
307 ('CompositorNodeColorSpill', 'COLOR_SPILL', 'Color Spill'),
308 ('CompositorNodeCryptomatte', 'CRYPTOMATTE', 'Cryptomatte'),
309 ('CompositorNodeDiffMatte', 'DIFF_MATTE', 'Difference Key'),
310 ('CompositorNodeDistanceMatte', 'DISTANCE_MATTE', 'Distance Key'),
311 ('CompositorNodeDoubleEdgeMask', 'DOUBLEEDGEMASK', 'Double Edge Mask'),
312 ('CompositorNodeEllipseMask', 'ELLIPSEMASK', 'Ellipse Mask'),
313 ('CompositorNodeKeying', 'KEYING', 'Keying'),
314 ('CompositorNodeKeyingScreen', 'KEYINGSCREEN', 'Keying Screen'),
315 ('CompositorNodeLumaMatte', 'LUMA_MATTE', 'Luminance Key'),
317 # (rna_type.identifier, type, rna_type.name)
318 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
319 # Keeping things in alphabetical order so we don't need to sort later.
320 compo_distort_nodes_props = (
321 ('CompositorNodeCornerPin', 'CORNERPIN', 'Corner Pin'),
322 ('CompositorNodeCrop', 'CROP', 'Crop'),
323 ('CompositorNodeDisplace', 'DISPLACE', 'Displace'),
324 ('CompositorNodeFlip', 'FLIP', 'Flip'),
325 ('CompositorNodeLensdist', 'LENSDIST', 'Lens Distortion'),
326 ('CompositorNodeMapUV', 'MAP_UV', 'Map UV'),
327 ('CompositorNodeMovieDistortion', 'MOVIEDISTORTION', 'Movie Distortion'),
328 ('CompositorNodePlaneTrackDeform', 'PLANETRACKDEFORM', 'Plane Track Deform'),
329 ('CompositorNodeRotate', 'ROTATE', 'Rotate'),
330 ('CompositorNodeScale', 'SCALE', 'Scale'),
331 ('CompositorNodeStabilize', 'STABILIZE2D', 'Stabilize 2D'),
332 ('CompositorNodeTransform', 'TRANSFORM', 'Transform'),
333 ('CompositorNodeTranslate', 'TRANSLATE', 'Translate'),
335 # (rna_type.identifier, type, rna_type.name)
336 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
337 # Keeping things in alphabetical order so we don't need to sort later.
338 compo_layout_nodes_props = (
339 ('NodeFrame', 'FRAME', 'Frame'),
340 ('NodeReroute', 'REROUTE', 'Reroute'),
341 ('CompositorNodeSwitch', 'SWITCH', 'Switch'),
343 # Blender Render material nodes
344 # (rna_type.identifier, type, rna_type.name)
345 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
346 blender_mat_input_nodes_props = (
347 ('ShaderNodeMaterial', 'MATERIAL', 'Material'),
348 ('ShaderNodeCameraData', 'CAMERA', 'Camera Data'),
349 ('ShaderNodeLightData', 'LIGHT', 'Light Data'),
350 ('ShaderNodeValue', 'VALUE', 'Value'),
351 ('ShaderNodeRGB', 'RGB', 'RGB'),
352 ('ShaderNodeTexture', 'TEXTURE', 'Texture'),
353 ('ShaderNodeGeometry', 'GEOMETRY', 'Geometry'),
354 ('ShaderNodeExtendedMaterial', 'MATERIAL_EXT', 'Extended Material'),
357 # (rna_type.identifier, type, rna_type.name)
358 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
359 blender_mat_output_nodes_props = (
360 ('ShaderNodeOutput', 'OUTPUT', 'Output'),
363 # (rna_type.identifier, type, rna_type.name)
364 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
365 blender_mat_color_nodes_props = (
366 ('ShaderNodeMixRGB', 'MIX_RGB', 'MixRGB'),
367 ('ShaderNodeRGBCurve', 'CURVE_RGB', 'RGB Curves'),
368 ('ShaderNodeInvert', 'INVERT', 'Invert'),
369 ('ShaderNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
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_vector_nodes_props = (
375 ('ShaderNodeNormal', 'NORMAL', 'Normal'),
376 ('ShaderNodeMapping', 'MAPPING', 'Mapping'),
377 ('ShaderNodeVectorCurve', 'CURVE_VEC', 'Vector Curves'),
380 # (rna_type.identifier, type, rna_type.name)
381 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
382 blender_mat_converter_nodes_props = (
383 ('ShaderNodeValToRGB', 'VALTORGB', 'ColorRamp'),
384 ('ShaderNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
385 ('ShaderNodeMath', 'MATH', 'Math'),
386 ('ShaderNodeVectorMath', 'VECT_MATH', 'Vector Math'),
387 ('ShaderNodeSqueeze', 'SQUEEZE', 'Squeeze Value'),
388 ('ShaderNodeSeparateRGB', 'SEPRGB', 'Separate RGB'),
389 ('ShaderNodeCombineRGB', 'COMBRGB', 'Combine RGB'),
390 ('ShaderNodeSeparateHSV', 'SEPHSV', 'Separate HSV'),
391 ('ShaderNodeCombineHSV', 'COMBHSV', 'Combine HSV'),
394 # (rna_type.identifier, type, rna_type.name)
395 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
396 blender_mat_layout_nodes_props = (
397 ('NodeReroute', 'REROUTE', 'Reroute'),
400 # Texture Nodes
401 # (rna_type.identifier, type, rna_type.name)
402 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
403 texture_input_nodes_props = (
404 ('TextureNodeCurveTime', 'CURVE_TIME', 'Curve Time'),
405 ('TextureNodeCoordinates', 'COORD', 'Coordinates'),
406 ('TextureNodeTexture', 'TEXTURE', 'Texture'),
407 ('TextureNodeImage', 'IMAGE', 'Image'),
410 # (rna_type.identifier, type, rna_type.name)
411 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
412 texture_output_nodes_props = (
413 ('TextureNodeOutput', 'OUTPUT', 'Output'),
414 ('TextureNodeViewer', 'VIEWER', 'Viewer'),
417 # (rna_type.identifier, type, rna_type.name)
418 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
419 texture_color_nodes_props = (
420 ('TextureNodeMixRGB', 'MIX_RGB', 'Mix RGB'),
421 ('TextureNodeCurveRGB', 'CURVE_RGB', 'RGB Curves'),
422 ('TextureNodeInvert', 'INVERT', 'Invert'),
423 ('TextureNodeHueSaturation', 'HUE_SAT', 'Hue/Saturation'),
424 ('TextureNodeCompose', 'COMPOSE', 'Combine RGBA'),
425 ('TextureNodeDecompose', 'DECOMPOSE', 'Separate RGBA'),
428 # (rna_type.identifier, type, rna_type.name)
429 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
430 texture_pattern_nodes_props = (
431 ('TextureNodeChecker', 'CHECKER', 'Checker'),
432 ('TextureNodeBricks', 'BRICKS', 'Bricks'),
435 # (rna_type.identifier, type, rna_type.name)
436 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
437 texture_textures_nodes_props = (
438 ('TextureNodeTexNoise', 'TEX_NOISE', 'Noise'),
439 ('TextureNodeTexDistNoise', 'TEX_DISTNOISE', 'Distorted Noise'),
440 ('TextureNodeTexClouds', 'TEX_CLOUDS', 'Clouds'),
441 ('TextureNodeTexBlend', 'TEX_BLEND', 'Blend'),
442 ('TextureNodeTexVoronoi', 'TEX_VORONOI', 'Voronoi'),
443 ('TextureNodeTexMagic', 'TEX_MAGIC', 'Magic'),
444 ('TextureNodeTexMarble', 'TEX_MARBLE', 'Marble'),
445 ('TextureNodeTexWood', 'TEX_WOOD', 'Wood'),
446 ('TextureNodeTexMusgrave', 'TEX_MUSGRAVE', 'Musgrave'),
447 ('TextureNodeTexStucci', 'TEX_STUCCI', 'Stucci'),
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_converter_nodes_props = (
453 ('TextureNodeMath', 'MATH', 'Math'),
454 ('TextureNodeValToRGB', 'VALTORGB', 'ColorRamp'),
455 ('TextureNodeRGBToBW', 'RGBTOBW', 'RGB to BW'),
456 ('TextureNodeValToNor', 'VALTONOR', 'Value to Normal'),
457 ('TextureNodeDistance', 'DISTANCE', 'Distance'),
460 # (rna_type.identifier, type, rna_type.name)
461 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
462 texture_distort_nodes_props = (
463 ('TextureNodeScale', 'SCALE', 'Scale'),
464 ('TextureNodeTranslate', 'TRANSLATE', 'Translate'),
465 ('TextureNodeRotate', 'ROTATE', 'Rotate'),
466 ('TextureNodeAt', 'AT', 'At'),
469 # (rna_type.identifier, type, rna_type.name)
470 # Keeping mixed case to avoid having to translate entries when adding new nodes in operators.
471 texture_layout_nodes_props = (
472 ('NodeReroute', 'REROUTE', 'Reroute'),
475 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
476 # used list, not tuple for easy merging with other lists.
477 blend_types = [
478 ('MIX', 'Mix', 'Mix Mode'),
479 ('ADD', 'Add', 'Add Mode'),
480 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
481 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
482 ('SCREEN', 'Screen', 'Screen Mode'),
483 ('DIVIDE', 'Divide', 'Divide Mode'),
484 ('DIFFERENCE', 'Difference', 'Difference Mode'),
485 ('DARKEN', 'Darken', 'Darken Mode'),
486 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
487 ('OVERLAY', 'Overlay', 'Overlay Mode'),
488 ('DODGE', 'Dodge', 'Dodge Mode'),
489 ('BURN', 'Burn', 'Burn Mode'),
490 ('HUE', 'Hue', 'Hue Mode'),
491 ('SATURATION', 'Saturation', 'Saturation Mode'),
492 ('VALUE', 'Value', 'Value Mode'),
493 ('COLOR', 'Color', 'Color Mode'),
494 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
495 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
498 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
499 # used list, not tuple for easy merging with other lists.
500 operations = [
501 ('ADD', 'Add', 'Add Mode'),
502 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
503 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
504 ('DIVIDE', 'Divide', 'Divide Mode'),
505 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
506 ('SINE', 'Sine', 'Sine Mode'),
507 ('COSINE', 'Cosine', 'Cosine Mode'),
508 ('TANGENT', 'Tangent', 'Tangent Mode'),
509 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
510 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
511 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
512 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
513 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
514 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
515 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
516 ('POWER', 'Power', 'Power Mode'),
517 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
518 ('SQRT', 'Square Root', 'Square Root Mode'),
519 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
520 ('EXPONENT', 'Exponent', 'Exponent Mode'),
521 ('MINIMUM', 'Minimum', 'Minimum Mode'),
522 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
523 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
524 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
525 ('SIGN', 'Sign', 'Sign Mode'),
526 ('COMPARE', 'Compare', 'Compare Mode'),
527 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
528 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
529 ('FRACT', 'Fraction', 'Fraction Mode'),
530 ('MODULO', 'Modulo', 'Modulo Mode'),
531 ('SNAP', 'Snap', 'Snap Mode'),
532 ('WRAP', 'Wrap', 'Wrap Mode'),
533 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
534 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
535 ('ROUND', 'Round', 'Round Mode'),
536 ('FLOOR', 'Floor', 'Floor Mode'),
537 ('CEIL', 'Ceil', 'Ceil Mode'),
538 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
539 ('RADIANS', 'To Radians', 'To Radians Mode'),
540 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
543 # Operations used by the geometry boolean node and join geometry node
544 geo_combine_operations = [
545 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
546 ('INTERSECT', 'Intersect', 'Intersect Mode'),
547 ('UNION', 'Union', 'Union Mode'),
548 ('DIFFERENCE', 'Difference', 'Difference Mode'),
551 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
552 # used list, not tuple for easy merging with other lists.
553 navs = [
554 ('CURRENT', 'Current', 'Leave at current state'),
555 ('NEXT', 'Next', 'Next blend type/operation'),
556 ('PREV', 'Prev', 'Previous blend type/operation'),
559 draw_color_sets = {
560 "red_white": (
561 (1.0, 1.0, 1.0, 0.7),
562 (1.0, 0.0, 0.0, 0.7),
563 (0.8, 0.2, 0.2, 1.0)
565 "green": (
566 (0.0, 0.0, 0.0, 1.0),
567 (0.38, 0.77, 0.38, 1.0),
568 (0.38, 0.77, 0.38, 1.0)
570 "yellow": (
571 (0.0, 0.0, 0.0, 1.0),
572 (0.77, 0.77, 0.16, 1.0),
573 (0.77, 0.77, 0.16, 1.0)
575 "purple": (
576 (0.0, 0.0, 0.0, 1.0),
577 (0.38, 0.38, 0.77, 1.0),
578 (0.38, 0.38, 0.77, 1.0)
580 "grey": (
581 (0.0, 0.0, 0.0, 1.0),
582 (0.63, 0.63, 0.63, 1.0),
583 (0.63, 0.63, 0.63, 1.0)
585 "black": (
586 (1.0, 1.0, 1.0, 0.7),
587 (0.0, 0.0, 0.0, 0.7),
588 (0.2, 0.2, 0.2, 1.0)
592 viewer_socket_name = "tmp_viewer"
594 def get_nodes_from_category(category_name, context):
595 for category in node_categories_iter(context):
596 if category.name == category_name:
597 return sorted(category.items(context), key=lambda node: node.label)
599 def is_visible_socket(socket):
600 return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
602 def nice_hotkey_name(punc):
603 # convert the ugly string name into the actual character
604 nice_name = {
605 'LEFTMOUSE': "LMB",
606 'MIDDLEMOUSE': "MMB",
607 'RIGHTMOUSE': "RMB",
608 'WHEELUPMOUSE': "Wheel Up",
609 'WHEELDOWNMOUSE': "Wheel Down",
610 'WHEELINMOUSE': "Wheel In",
611 'WHEELOUTMOUSE': "Wheel Out",
612 'ZERO': "0",
613 'ONE': "1",
614 'TWO': "2",
615 'THREE': "3",
616 'FOUR': "4",
617 'FIVE': "5",
618 'SIX': "6",
619 'SEVEN': "7",
620 'EIGHT': "8",
621 'NINE': "9",
622 'OSKEY': "Super",
623 'RET': "Enter",
624 'LINE_FEED': "Enter",
625 'SEMI_COLON': ";",
626 'PERIOD': ".",
627 'COMMA': ",",
628 'QUOTE': '"',
629 'MINUS': "-",
630 'SLASH': "/",
631 'BACK_SLASH': "\\",
632 'EQUAL': "=",
633 'NUMPAD_1': "Numpad 1",
634 'NUMPAD_2': "Numpad 2",
635 'NUMPAD_3': "Numpad 3",
636 'NUMPAD_4': "Numpad 4",
637 'NUMPAD_5': "Numpad 5",
638 'NUMPAD_6': "Numpad 6",
639 'NUMPAD_7': "Numpad 7",
640 'NUMPAD_8': "Numpad 8",
641 'NUMPAD_9': "Numpad 9",
642 'NUMPAD_0': "Numpad 0",
643 'NUMPAD_PERIOD': "Numpad .",
644 'NUMPAD_SLASH': "Numpad /",
645 'NUMPAD_ASTERIX': "Numpad *",
646 'NUMPAD_MINUS': "Numpad -",
647 'NUMPAD_ENTER': "Numpad Enter",
648 'NUMPAD_PLUS': "Numpad +",
650 try:
651 return nice_name[punc]
652 except KeyError:
653 return punc.replace("_", " ").title()
656 def force_update(context):
657 context.space_data.node_tree.update_tag()
660 def dpifac():
661 prefs = bpy.context.preferences.system
662 return prefs.dpi * prefs.pixel_size / 72
665 def node_mid_pt(node, axis):
666 if axis == 'x':
667 d = node.location.x + (node.dimensions.x / 2)
668 elif axis == 'y':
669 d = node.location.y - (node.dimensions.y / 2)
670 else:
671 d = 0
672 return d
675 def autolink(node1, node2, links):
676 link_made = False
677 available_inputs = [inp for inp in node2.inputs if inp.enabled]
678 available_outputs = [outp for outp in node1.outputs if outp.enabled]
679 for outp in available_outputs:
680 for inp in available_inputs:
681 if not inp.is_linked and inp.name == outp.name:
682 link_made = True
683 links.new(outp, inp)
684 return True
686 for outp in available_outputs:
687 for inp in available_inputs:
688 if not inp.is_linked and inp.type == outp.type:
689 link_made = True
690 links.new(outp, inp)
691 return True
693 # force some connection even if the type doesn't match
694 if available_outputs:
695 for inp in available_inputs:
696 if not inp.is_linked:
697 link_made = True
698 links.new(available_outputs[0], inp)
699 return True
701 # even if no sockets are open, force one of matching type
702 for outp in available_outputs:
703 for inp in available_inputs:
704 if inp.type == outp.type:
705 link_made = True
706 links.new(outp, inp)
707 return True
709 # do something!
710 for outp in available_outputs:
711 for inp in available_inputs:
712 link_made = True
713 links.new(outp, inp)
714 return True
716 print("Could not make a link from " + node1.name + " to " + node2.name)
717 return link_made
720 def node_at_pos(nodes, context, event):
721 nodes_under_mouse = []
722 target_node = None
724 store_mouse_cursor(context, event)
725 x, y = context.space_data.cursor_location
727 # Make a list of each corner (and middle of border) for each node.
728 # Will be sorted to find nearest point and thus nearest node
729 node_points_with_dist = []
730 for node in nodes:
731 skipnode = False
732 if node.type != 'FRAME': # no point trying to link to a frame node
733 locx = node.location.x
734 locy = node.location.y
735 dimx = node.dimensions.x/dpifac()
736 dimy = node.dimensions.y/dpifac()
737 if node.parent:
738 locx += node.parent.location.x
739 locy += node.parent.location.y
740 if node.parent.parent:
741 locx += node.parent.parent.location.x
742 locy += node.parent.parent.location.y
743 if node.parent.parent.parent:
744 locx += node.parent.parent.parent.location.x
745 locy += node.parent.parent.parent.location.y
746 if node.parent.parent.parent.parent:
747 # Support three levels or parenting
748 # There's got to be a better way to do this...
749 skipnode = True
750 if not skipnode:
751 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
752 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
753 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
754 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
756 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
757 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
758 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
759 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
761 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
763 for node in nodes:
764 if node.type != 'FRAME' and skipnode == False:
765 locx = node.location.x
766 locy = node.location.y
767 dimx = node.dimensions.x/dpifac()
768 dimy = node.dimensions.y/dpifac()
769 if node.parent:
770 locx += node.parent.location.x
771 locy += node.parent.location.y
772 if (locx <= x <= locx + dimx) and \
773 (locy - dimy <= y <= locy):
774 nodes_under_mouse.append(node)
776 if len(nodes_under_mouse) == 1:
777 if nodes_under_mouse[0] != nearest_node:
778 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
779 else:
780 target_node = nearest_node # else use the nearest node
781 else:
782 target_node = nearest_node
783 return target_node
786 def store_mouse_cursor(context, event):
787 space = context.space_data
788 v2d = context.region.view2d
789 tree = space.edit_tree
791 # convert mouse position to the View2D for later node placement
792 if context.region.type == 'WINDOW':
793 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
794 else:
795 space.cursor_location = tree.view_center
797 def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
798 shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
800 vertices = ((x1, y1), (x2, y2))
801 vertex_colors = ((colour[0]+(1.0-colour[0])/4,
802 colour[1]+(1.0-colour[1])/4,
803 colour[2]+(1.0-colour[2])/4,
804 colour[3]+(1.0-colour[3])/4),
805 colour)
807 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
808 bgl.glLineWidth(size * dpifac())
810 shader.bind()
811 batch.draw(shader)
814 def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
815 radius = radius * dpifac()
816 sides = 12
817 vertices = [(radius * cos(i * 2 * pi / sides) + mx,
818 radius * sin(i * 2 * pi / sides) + my)
819 for i in range(sides + 1)]
821 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
822 shader.bind()
823 shader.uniform_float("color", colour)
824 batch.draw(shader)
826 def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
827 area_width = bpy.context.area.width - (16*dpifac()) - 1
828 bottom_bar = (16*dpifac()) + 1
829 sides = 16
830 radius = radius*dpifac()
832 nlocx = (node.location.x+1)*dpifac()
833 nlocy = (node.location.y+1)*dpifac()
834 ndimx = node.dimensions.x
835 ndimy = node.dimensions.y
836 # This is a stupid way to do this... TODO use while loop
837 if node.parent:
838 nlocx += node.parent.location.x
839 nlocy += node.parent.location.y
840 if node.parent.parent:
841 nlocx += node.parent.parent.location.x
842 nlocy += node.parent.parent.location.y
843 if node.parent.parent.parent:
844 nlocx += node.parent.parent.parent.location.x
845 nlocy += node.parent.parent.parent.location.y
847 if node.hide:
848 nlocx += -1
849 nlocy += 5
850 if node.type == 'REROUTE':
851 #nlocx += 1
852 nlocy -= 1
853 ndimx = 0
854 ndimy = 0
855 radius += 6
857 # Top left corner
858 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
859 vertices = [(mx,my)]
860 for i in range(sides+1):
861 if (4<=i<=8):
862 if my > bottom_bar and mx < area_width:
863 cosine = radius * cos(i * 2 * pi / sides) + mx
864 sine = radius * sin(i * 2 * pi / sides) + my
865 vertices.append((cosine,sine))
866 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
867 shader.bind()
868 shader.uniform_float("color", colour)
869 batch.draw(shader)
871 # Top right corner
872 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
873 vertices = [(mx,my)]
874 for i in range(sides+1):
875 if (0<=i<=4):
876 if my > bottom_bar and mx < area_width:
877 cosine = radius * cos(i * 2 * pi / sides) + mx
878 sine = radius * sin(i * 2 * pi / sides) + my
879 vertices.append((cosine,sine))
880 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
881 shader.bind()
882 shader.uniform_float("color", colour)
883 batch.draw(shader)
885 # Bottom left corner
886 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
887 vertices = [(mx,my)]
888 for i in range(sides+1):
889 if (8<=i<=12):
890 if my > bottom_bar and mx < area_width:
891 cosine = radius * cos(i * 2 * pi / sides) + mx
892 sine = radius * sin(i * 2 * pi / sides) + my
893 vertices.append((cosine,sine))
894 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
895 shader.bind()
896 shader.uniform_float("color", colour)
897 batch.draw(shader)
899 # Bottom right corner
900 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
901 vertices = [(mx,my)]
902 for i in range(sides+1):
903 if (12<=i<=16):
904 if my > bottom_bar and mx < area_width:
905 cosine = radius * cos(i * 2 * pi / sides) + mx
906 sine = radius * sin(i * 2 * pi / sides) + my
907 vertices.append((cosine,sine))
908 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
909 shader.bind()
910 shader.uniform_float("color", colour)
911 batch.draw(shader)
913 # prepare drawing all edges in one batch
914 vertices = []
915 indices = []
916 id_last = 0
918 # Left edge
919 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
920 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
921 if m1x < area_width and m2x < area_width:
922 vertices.extend([(m2x-radius,m2y), (m2x,m2y),
923 (m1x,m1y), (m1x-radius,m1y)])
924 indices.extend([(id_last, id_last+1, id_last+3),
925 (id_last+3, id_last+1, id_last+2)])
926 id_last += 4
928 # Top edge
929 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
930 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
931 m1x = min(m1x, area_width)
932 m2x = min(m2x, area_width)
933 if m1y > bottom_bar and m2y > bottom_bar:
934 vertices.extend([(m1x,m1y), (m2x,m1y),
935 (m2x,m1y+radius), (m1x,m1y+radius)])
936 indices.extend([(id_last, id_last+1, id_last+3),
937 (id_last+3, id_last+1, id_last+2)])
938 id_last += 4
940 # Right edge
941 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
942 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
943 m1y = max(m1y, bottom_bar)
944 m2y = max(m2y, bottom_bar)
945 if m1x < area_width and m2x < area_width:
946 vertices.extend([(m1x,m2y), (m1x+radius,m2y),
947 (m1x+radius,m1y), (m1x,m1y)])
948 indices.extend([(id_last, id_last+1, id_last+3),
949 (id_last+3, id_last+1, id_last+2)])
950 id_last += 4
952 # Bottom edge
953 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
954 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
955 m1x = min(m1x, area_width)
956 m2x = min(m2x, area_width)
957 if m1y > bottom_bar and m2y > bottom_bar:
958 vertices.extend([(m1x,m2y), (m2x,m2y),
959 (m2x,m1y-radius), (m1x,m1y-radius)])
960 indices.extend([(id_last, id_last+1, id_last+3),
961 (id_last+3, id_last+1, id_last+2)])
963 # now draw all edges in one batch
964 if len(vertices) != 0:
965 batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
966 shader.bind()
967 shader.uniform_float("color", colour)
968 batch.draw(shader)
970 def draw_callback_nodeoutline(self, context, mode):
971 if self.mouse_path:
973 bgl.glLineWidth(1)
974 bgl.glEnable(bgl.GL_BLEND)
975 bgl.glEnable(bgl.GL_LINE_SMOOTH)
976 bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
978 nodes, links = get_nodes_links(context)
980 shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
982 if mode == "LINK":
983 col_outer = (1.0, 0.2, 0.2, 0.4)
984 col_inner = (0.0, 0.0, 0.0, 0.5)
985 col_circle_inner = (0.3, 0.05, 0.05, 1.0)
986 elif mode == "LINKMENU":
987 col_outer = (0.4, 0.6, 1.0, 0.4)
988 col_inner = (0.0, 0.0, 0.0, 0.5)
989 col_circle_inner = (0.08, 0.15, .3, 1.0)
990 elif mode == "MIX":
991 col_outer = (0.2, 1.0, 0.2, 0.4)
992 col_inner = (0.0, 0.0, 0.0, 0.5)
993 col_circle_inner = (0.05, 0.3, 0.05, 1.0)
995 m1x = self.mouse_path[0][0]
996 m1y = self.mouse_path[0][1]
997 m2x = self.mouse_path[-1][0]
998 m2y = self.mouse_path[-1][1]
1000 n1 = nodes[context.scene.NWLazySource]
1001 n2 = nodes[context.scene.NWLazyTarget]
1003 if n1 == n2:
1004 col_outer = (0.4, 0.4, 0.4, 0.4)
1005 col_inner = (0.0, 0.0, 0.0, 0.5)
1006 col_circle_inner = (0.2, 0.2, 0.2, 1.0)
1008 draw_rounded_node_border(shader, n1, radius=6, colour=col_outer) # outline
1009 draw_rounded_node_border(shader, n1, radius=5, colour=col_inner) # inner
1010 draw_rounded_node_border(shader, n2, radius=6, colour=col_outer) # outline
1011 draw_rounded_node_border(shader, n2, radius=5, colour=col_inner) # inner
1013 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
1014 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
1016 # circle outline
1017 draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
1018 draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
1020 # circle inner
1021 draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
1022 draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
1024 bgl.glDisable(bgl.GL_BLEND)
1025 bgl.glDisable(bgl.GL_LINE_SMOOTH)
1026 def get_active_tree(context):
1027 tree = context.space_data.node_tree
1028 path = []
1029 # Get nodes from currently edited tree.
1030 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1031 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1032 # the same as context.active_node, the user is in a group.
1033 # Check recursively until we find the real active node_tree:
1034 if tree.nodes.active:
1035 while tree.nodes.active != context.active_node:
1036 tree = tree.nodes.active.node_tree
1037 path.append(tree)
1038 return tree, path
1040 def get_nodes_links(context):
1041 tree, path = get_active_tree(context)
1042 return tree.nodes, tree.links
1044 def is_viewer_socket(socket):
1045 # checks if a internal socket is a valid viewer socket
1046 return socket.name == viewer_socket_name and socket.NWViewerSocket
1048 def get_internal_socket(socket):
1049 #get the internal socket from a socket inside or outside the group
1050 node = socket.node
1051 if node.type == 'GROUP_OUTPUT':
1052 source_iterator = node.inputs
1053 iterator = node.id_data.outputs
1054 elif node.type == 'GROUP_INPUT':
1055 source_iterator = node.outputs
1056 iterator = node.id_data.inputs
1057 elif hasattr(node, "node_tree"):
1058 if socket.is_output:
1059 source_iterator = node.outputs
1060 iterator = node.node_tree.outputs
1061 else:
1062 source_iterator = node.inputs
1063 iterator = node.node_tree.inputs
1064 else:
1065 return None
1067 for i, s in enumerate(source_iterator):
1068 if s == socket:
1069 break
1070 return iterator[i]
1072 def is_viewer_link(link, output_node):
1073 if "Emission Viewer" in link.to_node.name or link.to_node == output_node and link.to_socket == output_node.inputs[0]:
1074 return True
1075 if link.to_node.type == 'GROUP_OUTPUT':
1076 socket = get_internal_socket(link.to_socket)
1077 if is_viewer_socket(socket):
1078 return True
1079 return False
1081 def get_group_output_node(tree):
1082 for node in tree.nodes:
1083 if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
1084 return node
1086 def get_output_location(tree):
1087 # get right-most location
1088 sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
1089 max_xloc_node = sorted_by_xloc[-1]
1090 if max_xloc_node.name == 'Emission Viewer':
1091 max_xloc_node = sorted_by_xloc[-2]
1093 # get average y location
1094 sum_yloc = 0
1095 for node in tree.nodes:
1096 sum_yloc += node.location.y
1098 loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1099 loc_y = sum_yloc / len(tree.nodes)
1100 return loc_x, loc_y
1102 # Principled prefs
1103 class NWPrincipledPreferences(bpy.types.PropertyGroup):
1104 base_color: StringProperty(
1105 name='Base Color',
1106 default='diffuse diff albedo base col color',
1107 description='Naming Components for Base Color maps')
1108 sss_color: StringProperty(
1109 name='Subsurface Color',
1110 default='sss subsurface',
1111 description='Naming Components for Subsurface Color maps')
1112 metallic: StringProperty(
1113 name='Metallic',
1114 default='metallic metalness metal mtl',
1115 description='Naming Components for metallness maps')
1116 specular: StringProperty(
1117 name='Specular',
1118 default='specularity specular spec spc',
1119 description='Naming Components for Specular maps')
1120 normal: StringProperty(
1121 name='Normal',
1122 default='normal nor nrm nrml norm',
1123 description='Naming Components for Normal maps')
1124 bump: StringProperty(
1125 name='Bump',
1126 default='bump bmp',
1127 description='Naming Components for bump maps')
1128 rough: StringProperty(
1129 name='Roughness',
1130 default='roughness rough rgh',
1131 description='Naming Components for roughness maps')
1132 gloss: StringProperty(
1133 name='Gloss',
1134 default='gloss glossy glossiness',
1135 description='Naming Components for glossy maps')
1136 displacement: StringProperty(
1137 name='Displacement',
1138 default='displacement displace disp dsp height heightmap',
1139 description='Naming Components for displacement maps')
1141 # Addon prefs
1142 class NWNodeWrangler(bpy.types.AddonPreferences):
1143 bl_idname = __name__
1145 merge_hide: EnumProperty(
1146 name="Hide Mix nodes",
1147 items=(
1148 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1149 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1150 ("NEVER", "Never", "Never collapse the new merge nodes")
1152 default='NON_SHADER',
1153 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1154 merge_position: EnumProperty(
1155 name="Mix Node Position",
1156 items=(
1157 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1158 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1160 default='CENTER',
1161 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1163 show_hotkey_list: BoolProperty(
1164 name="Show Hotkey List",
1165 default=False,
1166 description="Expand this box into a list of all the hotkeys for functions in this addon"
1168 hotkey_list_filter: StringProperty(
1169 name=" Filter by Name",
1170 default="",
1171 description="Show only hotkeys that have this text in their name"
1173 show_principled_lists: BoolProperty(
1174 name="Show Principled naming tags",
1175 default=False,
1176 description="Expand this box into a list of all naming tags for principled texture setup"
1178 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
1180 def draw(self, context):
1181 layout = self.layout
1182 col = layout.column()
1183 col.prop(self, "merge_position")
1184 col.prop(self, "merge_hide")
1186 box = layout.box()
1187 col = box.column(align=True)
1188 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
1189 if self.show_principled_lists:
1190 tags = self.principled_tags
1192 col.prop(tags, "base_color")
1193 col.prop(tags, "sss_color")
1194 col.prop(tags, "metallic")
1195 col.prop(tags, "specular")
1196 col.prop(tags, "rough")
1197 col.prop(tags, "gloss")
1198 col.prop(tags, "normal")
1199 col.prop(tags, "bump")
1200 col.prop(tags, "displacement")
1202 box = layout.box()
1203 col = box.column(align=True)
1204 hotkey_button_name = "Show Hotkey List"
1205 if self.show_hotkey_list:
1206 hotkey_button_name = "Hide Hotkey List"
1207 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
1208 if self.show_hotkey_list:
1209 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
1210 col.separator()
1211 for hotkey in kmi_defs:
1212 if hotkey[7]:
1213 hotkey_name = hotkey[7]
1215 if self.hotkey_list_filter.lower() in hotkey_name.lower():
1216 row = col.row(align=True)
1217 row.label(text=hotkey_name)
1218 keystr = nice_hotkey_name(hotkey[1])
1219 if hotkey[4]:
1220 keystr = "Shift " + keystr
1221 if hotkey[5]:
1222 keystr = "Alt " + keystr
1223 if hotkey[3]:
1224 keystr = "Ctrl " + keystr
1225 row.label(text=keystr)
1229 def nw_check(context):
1230 space = context.space_data
1231 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1233 valid = False
1234 if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
1235 valid = True
1237 return valid
1239 class NWBase:
1240 @classmethod
1241 def poll(cls, context):
1242 return nw_check(context)
1245 # OPERATORS
1246 class NWLazyMix(Operator, NWBase):
1247 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1248 bl_idname = "node.nw_lazy_mix"
1249 bl_label = "Mix Nodes"
1250 bl_options = {'REGISTER', 'UNDO'}
1252 def modal(self, context, event):
1253 context.area.tag_redraw()
1254 nodes, links = get_nodes_links(context)
1255 cont = True
1257 start_pos = [event.mouse_region_x, event.mouse_region_y]
1259 node1 = None
1260 if not context.scene.NWBusyDrawing:
1261 node1 = node_at_pos(nodes, context, event)
1262 if node1:
1263 context.scene.NWBusyDrawing = node1.name
1264 else:
1265 if context.scene.NWBusyDrawing != 'STOP':
1266 node1 = nodes[context.scene.NWBusyDrawing]
1268 context.scene.NWLazySource = node1.name
1269 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1271 if event.type == 'MOUSEMOVE':
1272 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1274 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1275 end_pos = [event.mouse_region_x, event.mouse_region_y]
1276 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1278 node2 = None
1279 node2 = node_at_pos(nodes, context, event)
1280 if node2:
1281 context.scene.NWBusyDrawing = node2.name
1283 if node1 == node2:
1284 cont = False
1286 if cont:
1287 if node1 and node2:
1288 for node in nodes:
1289 node.select = False
1290 node1.select = True
1291 node2.select = True
1293 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
1295 context.scene.NWBusyDrawing = ""
1296 return {'FINISHED'}
1298 elif event.type == 'ESC':
1299 print('cancelled')
1300 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1301 return {'CANCELLED'}
1303 return {'RUNNING_MODAL'}
1305 def invoke(self, context, event):
1306 if context.area.type == 'NODE_EDITOR':
1307 # the arguments we pass the the callback
1308 args = (self, context, 'MIX')
1309 # Add the region OpenGL drawing callback
1310 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1311 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1313 self.mouse_path = []
1315 context.window_manager.modal_handler_add(self)
1316 return {'RUNNING_MODAL'}
1317 else:
1318 self.report({'WARNING'}, "View3D not found, cannot run operator")
1319 return {'CANCELLED'}
1322 class NWLazyConnect(Operator, NWBase):
1323 """Connect two nodes without clicking a specific socket (automatically determined"""
1324 bl_idname = "node.nw_lazy_connect"
1325 bl_label = "Lazy Connect"
1326 bl_options = {'REGISTER', 'UNDO'}
1327 with_menu: BoolProperty()
1329 def modal(self, context, event):
1330 context.area.tag_redraw()
1331 nodes, links = get_nodes_links(context)
1332 cont = True
1334 start_pos = [event.mouse_region_x, event.mouse_region_y]
1336 node1 = None
1337 if not context.scene.NWBusyDrawing:
1338 node1 = node_at_pos(nodes, context, event)
1339 if node1:
1340 context.scene.NWBusyDrawing = node1.name
1341 else:
1342 if context.scene.NWBusyDrawing != 'STOP':
1343 node1 = nodes[context.scene.NWBusyDrawing]
1345 context.scene.NWLazySource = node1.name
1346 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1348 if event.type == 'MOUSEMOVE':
1349 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1351 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1352 end_pos = [event.mouse_region_x, event.mouse_region_y]
1353 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1355 node2 = None
1356 node2 = node_at_pos(nodes, context, event)
1357 if node2:
1358 context.scene.NWBusyDrawing = node2.name
1360 if node1 == node2:
1361 cont = False
1363 link_success = False
1364 if cont:
1365 if node1 and node2:
1366 original_sel = []
1367 original_unsel = []
1368 for node in nodes:
1369 if node.select == True:
1370 node.select = False
1371 original_sel.append(node)
1372 else:
1373 original_unsel.append(node)
1374 node1.select = True
1375 node2.select = True
1377 #link_success = autolink(node1, node2, links)
1378 if self.with_menu:
1379 if len(node1.outputs) > 1 and node2.inputs:
1380 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1381 elif len(node1.outputs) == 1:
1382 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1383 else:
1384 link_success = autolink(node1, node2, links)
1386 for node in original_sel:
1387 node.select = True
1388 for node in original_unsel:
1389 node.select = False
1391 if link_success:
1392 force_update(context)
1393 context.scene.NWBusyDrawing = ""
1394 return {'FINISHED'}
1396 elif event.type == 'ESC':
1397 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1398 return {'CANCELLED'}
1400 return {'RUNNING_MODAL'}
1402 def invoke(self, context, event):
1403 if context.area.type == 'NODE_EDITOR':
1404 nodes, links = get_nodes_links(context)
1405 node = node_at_pos(nodes, context, event)
1406 if node:
1407 context.scene.NWBusyDrawing = node.name
1409 # the arguments we pass the the callback
1410 mode = "LINK"
1411 if self.with_menu:
1412 mode = "LINKMENU"
1413 args = (self, context, mode)
1414 # Add the region OpenGL drawing callback
1415 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1416 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1418 self.mouse_path = []
1420 context.window_manager.modal_handler_add(self)
1421 return {'RUNNING_MODAL'}
1422 else:
1423 self.report({'WARNING'}, "View3D not found, cannot run operator")
1424 return {'CANCELLED'}
1427 class NWDeleteUnused(Operator, NWBase):
1428 """Delete all nodes whose output is not used"""
1429 bl_idname = 'node.nw_del_unused'
1430 bl_label = 'Delete Unused Nodes'
1431 bl_options = {'REGISTER', 'UNDO'}
1433 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1434 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1436 def is_unused_node(self, node):
1437 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1438 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1439 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1440 if node.type in end_types:
1441 return False
1443 for output in node.outputs:
1444 if output.links:
1445 return False
1446 return True
1448 @classmethod
1449 def poll(cls, context):
1450 valid = False
1451 if nw_check(context):
1452 if context.space_data.node_tree.nodes:
1453 valid = True
1454 return valid
1456 def execute(self, context):
1457 nodes, links = get_nodes_links(context)
1459 # Store selection
1460 selection = []
1461 for node in nodes:
1462 if node.select == True:
1463 selection.append(node.name)
1465 for node in nodes:
1466 node.select = False
1468 deleted_nodes = []
1469 temp_deleted_nodes = []
1470 del_unused_iterations = len(nodes)
1471 for it in range(0, del_unused_iterations):
1472 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1473 for node in nodes:
1474 if self.is_unused_node(node):
1475 node.select = True
1476 deleted_nodes.append(node.name)
1477 bpy.ops.node.delete()
1479 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1480 break
1482 if self.delete_frames:
1483 repeat = True
1484 while repeat:
1485 frames_in_use = []
1486 frames = []
1487 repeat = False
1488 for node in nodes:
1489 if node.parent:
1490 frames_in_use.append(node.parent)
1491 for node in nodes:
1492 if node.type == 'FRAME' and node not in frames_in_use:
1493 frames.append(node)
1494 if node.parent:
1495 repeat = True # repeat for nested frames
1496 for node in frames:
1497 if node not in frames_in_use:
1498 node.select = True
1499 deleted_nodes.append(node.name)
1500 bpy.ops.node.delete()
1502 if self.delete_muted:
1503 for node in nodes:
1504 if node.mute:
1505 node.select = True
1506 deleted_nodes.append(node.name)
1507 bpy.ops.node.delete_reconnect()
1509 # get unique list of deleted nodes (iterations would count the same node more than once)
1510 deleted_nodes = list(set(deleted_nodes))
1511 for n in deleted_nodes:
1512 self.report({'INFO'}, "Node " + n + " deleted")
1513 num_deleted = len(deleted_nodes)
1514 n = ' node'
1515 if num_deleted > 1:
1516 n += 's'
1517 if num_deleted:
1518 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1519 else:
1520 self.report({'INFO'}, "Nothing deleted")
1522 # Restore selection
1523 nodes, links = get_nodes_links(context)
1524 for node in nodes:
1525 if node.name in selection:
1526 node.select = True
1527 return {'FINISHED'}
1529 def invoke(self, context, event):
1530 return context.window_manager.invoke_confirm(self, event)
1533 class NWSwapLinks(Operator, NWBase):
1534 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1535 bl_idname = 'node.nw_swap_links'
1536 bl_label = 'Swap Links'
1537 bl_options = {'REGISTER', 'UNDO'}
1539 @classmethod
1540 def poll(cls, context):
1541 valid = False
1542 if nw_check(context):
1543 if context.selected_nodes:
1544 valid = len(context.selected_nodes) <= 2
1545 return valid
1547 def execute(self, context):
1548 nodes, links = get_nodes_links(context)
1549 selected_nodes = context.selected_nodes
1550 n1 = selected_nodes[0]
1552 # Swap outputs
1553 if len(selected_nodes) == 2:
1554 n2 = selected_nodes[1]
1555 if n1.outputs and n2.outputs:
1556 n1_outputs = []
1557 n2_outputs = []
1559 out_index = 0
1560 for output in n1.outputs:
1561 if output.links:
1562 for link in output.links:
1563 n1_outputs.append([out_index, link.to_socket])
1564 links.remove(link)
1565 out_index += 1
1567 out_index = 0
1568 for output in n2.outputs:
1569 if output.links:
1570 for link in output.links:
1571 n2_outputs.append([out_index, link.to_socket])
1572 links.remove(link)
1573 out_index += 1
1575 for connection in n1_outputs:
1576 try:
1577 links.new(n2.outputs[connection[0]], connection[1])
1578 except:
1579 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1580 for connection in n2_outputs:
1581 try:
1582 links.new(n1.outputs[connection[0]], connection[1])
1583 except:
1584 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1585 else:
1586 if n1.outputs or n2.outputs:
1587 self.report({'WARNING'}, "One of the nodes has no outputs!")
1588 else:
1589 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1591 # Swap Inputs
1592 elif len(selected_nodes) == 1:
1593 if n1.inputs and n1.inputs[0].is_multi_input:
1594 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1595 return {'FINISHED'}
1596 if n1.inputs:
1597 types = []
1599 for i1 in n1.inputs:
1600 if i1.is_linked and not i1.is_multi_input:
1601 similar_types = 0
1602 for i2 in n1.inputs:
1603 if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
1604 similar_types += 1
1605 types.append ([i1, similar_types, i])
1606 i += 1
1607 types.sort(key=lambda k: k[1], reverse=True)
1609 if types:
1610 t = types[0]
1611 if t[1] == 2:
1612 for i2 in n1.inputs:
1613 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1614 pair = [t[0], i2]
1615 i1f = pair[0].links[0].from_socket
1616 i1t = pair[0].links[0].to_socket
1617 i2f = pair[1].links[0].from_socket
1618 i2t = pair[1].links[0].to_socket
1619 links.new(i1f, i2t)
1620 links.new(i2f, i1t)
1621 if t[1] == 1:
1622 if len(types) == 1:
1623 fs = t[0].links[0].from_socket
1624 i = t[2]
1625 links.remove(t[0].links[0])
1626 if i+1 == len(n1.inputs):
1627 i = -1
1628 i += 1
1629 while n1.inputs[i].is_linked:
1630 i += 1
1631 links.new(fs, n1.inputs[i])
1632 elif len(types) == 2:
1633 i1f = types[0][0].links[0].from_socket
1634 i1t = types[0][0].links[0].to_socket
1635 i2f = types[1][0].links[0].from_socket
1636 i2t = types[1][0].links[0].to_socket
1637 links.new(i1f, i2t)
1638 links.new(i2f, i1t)
1640 else:
1641 self.report({'WARNING'}, "This node has no input connections to swap!")
1642 else:
1643 self.report({'WARNING'}, "This node has no inputs to swap!")
1645 force_update(context)
1646 return {'FINISHED'}
1649 class NWResetBG(Operator, NWBase):
1650 """Reset the zoom and position of the background image"""
1651 bl_idname = 'node.nw_bg_reset'
1652 bl_label = 'Reset Backdrop'
1653 bl_options = {'REGISTER', 'UNDO'}
1655 @classmethod
1656 def poll(cls, context):
1657 valid = False
1658 if nw_check(context):
1659 snode = context.space_data
1660 valid = snode.tree_type == 'CompositorNodeTree'
1661 return valid
1663 def execute(self, context):
1664 context.space_data.backdrop_zoom = 1
1665 context.space_data.backdrop_offset[0] = 0
1666 context.space_data.backdrop_offset[1] = 0
1667 return {'FINISHED'}
1670 class NWAddAttrNode(Operator, NWBase):
1671 """Add an Attribute node with this name"""
1672 bl_idname = 'node.nw_add_attr_node'
1673 bl_label = 'Add UV map'
1674 bl_options = {'REGISTER', 'UNDO'}
1676 attr_name: StringProperty()
1678 def execute(self, context):
1679 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1680 nodes, links = get_nodes_links(context)
1681 nodes.active.attribute_name = self.attr_name
1682 return {'FINISHED'}
1684 class NWPreviewNode(Operator, NWBase):
1685 bl_idname = "node.nw_preview_node"
1686 bl_label = "Preview Node"
1687 bl_description = "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output"
1688 bl_options = {'REGISTER', 'UNDO'}
1690 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1691 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1692 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1693 run_in_geometry_nodes: BoolProperty(default=True)
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' or space.tree_type == 'GeometryNodeTree':
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 # So that we don't keep on adding new group outputs
1776 groupout.is_active_output = True
1777 return groupout
1779 @classmethod
1780 def search_sockets(cls, node, sockets, index=None):
1781 # recursively scan nodes for viewer sockets and store in list
1782 for i, input_socket in enumerate(node.inputs):
1783 if index and i != index:
1784 continue
1785 if len(input_socket.links):
1786 link = input_socket.links[0]
1787 next_node = link.from_node
1788 external_socket = link.from_socket
1789 if hasattr(next_node, "node_tree"):
1790 for socket_index, s in enumerate(next_node.outputs):
1791 if s == external_socket:
1792 break
1793 socket = next_node.node_tree.outputs[socket_index]
1794 if is_viewer_socket(socket) and socket not in sockets:
1795 sockets.append(socket)
1796 #continue search inside of node group but restrict socket to where we came from
1797 groupout = get_group_output_node(next_node.node_tree)
1798 cls.search_sockets(groupout, sockets, index=socket_index)
1800 @classmethod
1801 def scan_nodes(cls, tree, sockets):
1802 # get all viewer sockets in a material tree
1803 for node in tree.nodes:
1804 if hasattr(node, "node_tree"):
1805 for socket in node.node_tree.outputs:
1806 if is_viewer_socket(socket) and (socket not in sockets):
1807 sockets.append(socket)
1808 cls.scan_nodes(node.node_tree, sockets)
1810 def link_leads_to_used_socket(self, link):
1811 #return True if link leads to a socket that is already used in this material
1812 socket = get_internal_socket(link.to_socket)
1813 return (socket and self.is_socket_used_active_mat(socket))
1815 def is_socket_used_active_mat(self, socket):
1816 #ensure used sockets in active material is calculated and check given socket
1817 if not hasattr(self, "used_viewer_sockets_active_mat"):
1818 self.used_viewer_sockets_active_mat = []
1819 materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
1820 if materialout:
1821 emission = self.get_viewer_node(materialout)
1822 self.search_sockets((emission if emission else materialout), self.used_viewer_sockets_active_mat)
1823 return socket in self.used_viewer_sockets_active_mat
1825 def is_socket_used_other_mats(self, socket):
1826 #ensure used sockets in other materials are calculated and check given socket
1827 if not hasattr(self, "used_viewer_sockets_other_mats"):
1828 self.used_viewer_sockets_other_mats = []
1829 for mat in bpy.data.materials:
1830 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
1831 continue
1832 # get viewer node
1833 materialout = self.get_shader_output_node(mat.node_tree)
1834 if materialout:
1835 emission = self.get_viewer_node(materialout)
1836 self.search_sockets((emission if emission else materialout), self.used_viewer_sockets_other_mats)
1837 return socket in self.used_viewer_sockets_other_mats
1839 @staticmethod
1840 def get_viewer_node(materialout):
1841 input_socket = materialout.inputs[0]
1842 if len(input_socket.links) > 0:
1843 node = input_socket.links[0].from_node
1844 if node.type == 'EMISSION' and node.name == "Emission Viewer":
1845 return node
1847 def invoke(self, context, event):
1848 space = context.space_data
1849 # Ignore operator when running in wrong context.
1850 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
1851 return {'PASS_THROUGH'}
1853 shader_type = space.shader_type
1854 self.init_shader_variables(space, shader_type)
1855 shader_types = [x[1] for x in shaders_shader_nodes_props]
1856 mlocx = event.mouse_region_x
1857 mlocy = event.mouse_region_y
1858 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
1859 if 'FINISHED' in select_node: # only run if mouse click is on a node
1860 active_tree, path_to_tree = get_active_tree(context)
1861 nodes, links = active_tree.nodes, active_tree.links
1862 base_node_tree = space.node_tree
1863 active = nodes.active
1865 # For geometry node trees we just connect to the group output,
1866 # because there is no "viewer node" yet.
1867 if space.tree_type == "GeometryNodeTree":
1868 valid = False
1869 if active:
1870 for out in active.outputs:
1871 if is_visible_socket(out):
1872 valid = True
1873 break
1874 # Exit early
1875 if not valid:
1876 return {'FINISHED'}
1878 delete_sockets = []
1880 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1881 self.scan_nodes(base_node_tree, delete_sockets)
1883 # Find (or create if needed) the output of this node tree
1884 geometryoutput = self.ensure_group_output(base_node_tree)
1886 # Analyze outputs, make links
1887 out_i = None
1888 valid_outputs = []
1889 for i, out in enumerate(active.outputs):
1890 if is_visible_socket(out) and out.type == 'GEOMETRY':
1891 valid_outputs.append(i)
1892 if valid_outputs:
1893 out_i = valid_outputs[0] # Start index of node's outputs
1894 for i, valid_i in enumerate(valid_outputs):
1895 for out_link in active.outputs[valid_i].links:
1896 if is_viewer_link(out_link, geometryoutput):
1897 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1898 if i < len(valid_outputs) - 1:
1899 out_i = valid_outputs[i + 1]
1900 else:
1901 out_i = valid_outputs[0]
1903 make_links = [] # store sockets for new links
1904 delete_nodes = [] # store unused nodes to delete in the end
1905 if active.outputs:
1906 # If there is no 'GEOMETRY' output type - We can't preview the node
1907 if out_i is None:
1908 return {'FINISHED'}
1909 socket_type = 'GEOMETRY'
1910 # Find an input socket of the output of type geometry
1911 geometryoutindex = None
1912 for i,inp in enumerate(geometryoutput.inputs):
1913 if inp.type == socket_type:
1914 geometryoutindex = i
1915 break
1916 if geometryoutindex is None:
1917 # Create geometry socket
1918 geometryoutput.inputs.new(socket_type, 'Geometry')
1919 geometryoutindex = len(geometryoutput.inputs) - 1
1921 make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
1922 output_socket = geometryoutput.inputs[geometryoutindex]
1923 for li_from, li_to in make_links:
1924 base_node_tree.links.new(li_from, li_to)
1925 tree = base_node_tree
1926 link_end = output_socket
1927 while tree.nodes.active != active:
1928 node = tree.nodes.active
1929 index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1930 link_start = node.outputs[index]
1931 node_socket = node.node_tree.outputs[index]
1932 if node_socket in delete_sockets:
1933 delete_sockets.remove(node_socket)
1934 tree.links.new(link_start, link_end)
1935 # Iterate
1936 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1937 tree = tree.nodes.active.node_tree
1938 tree.links.new(active.outputs[out_i], link_end)
1940 # Delete sockets
1941 for socket in delete_sockets:
1942 tree = socket.id_data
1943 tree.outputs.remove(socket)
1945 # Delete nodes
1946 for tree, node in delete_nodes:
1947 tree.nodes.remove(node)
1949 nodes.active = active
1950 active.select = True
1951 force_update(context)
1952 return {'FINISHED'}
1955 # What follows is code for the shader editor
1956 output_types = [x[1] for x in shaders_output_nodes_props]
1957 valid = False
1958 if active:
1959 if (active.name != "Emission Viewer") and (active.type not in output_types):
1960 for out in active.outputs:
1961 if is_visible_socket(out):
1962 valid = True
1963 break
1964 if valid:
1965 # get material_output node
1966 materialout = None # placeholder node
1967 delete_sockets = []
1969 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1970 self.scan_nodes(base_node_tree, delete_sockets)
1972 materialout = self.get_shader_output_node(base_node_tree)
1973 if not materialout:
1974 materialout = base_node_tree.nodes.new(self.shader_output_ident)
1975 materialout.location = get_output_location(base_node_tree)
1976 materialout.select = False
1977 # Analyze outputs, add "Emission Viewer" if needed, make links
1978 out_i = None
1979 valid_outputs = []
1980 for i, out in enumerate(active.outputs):
1981 if is_visible_socket(out):
1982 valid_outputs.append(i)
1983 if valid_outputs:
1984 out_i = valid_outputs[0] # Start index of node's outputs
1985 for i, valid_i in enumerate(valid_outputs):
1986 for out_link in active.outputs[valid_i].links:
1987 if is_viewer_link(out_link, materialout):
1988 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1989 if i < len(valid_outputs) - 1:
1990 out_i = valid_outputs[i + 1]
1991 else:
1992 out_i = valid_outputs[0]
1994 make_links = [] # store sockets for new links
1995 delete_nodes = [] # store unused nodes to delete in the end
1996 if active.outputs:
1997 # If output type not 'SHADER' - "Emission Viewer" needed
1998 if active.outputs[out_i].type != 'SHADER':
1999 socket_type = 'NodeSocketColor'
2000 # get Emission Viewer node
2001 emission_exists = False
2002 emission_placeholder = base_node_tree.nodes[0]
2003 for node in base_node_tree.nodes:
2004 if "Emission Viewer" in node.name:
2005 emission_exists = True
2006 emission_placeholder = node
2007 if not emission_exists:
2008 emission = base_node_tree.nodes.new(self.shader_viewer_ident)
2009 emission.hide = True
2010 emission.location = [materialout.location.x, (materialout.location.y + 40)]
2011 emission.label = "Viewer"
2012 emission.name = "Emission Viewer"
2013 emission.use_custom_color = True
2014 emission.color = (0.6, 0.5, 0.4)
2015 emission.select = False
2016 else:
2017 emission = emission_placeholder
2018 output_socket = emission.inputs[0]
2020 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2021 if emission.outputs[0].links.__len__() > 0:
2022 if not emission.outputs[0].links[0].to_node == materialout:
2023 make_links.append((emission.outputs[0], materialout.inputs[0]))
2024 else:
2025 make_links.append((emission.outputs[0], materialout.inputs[0]))
2027 # Set brightness of viewer to compensate for Film and CM exposure
2028 if context.scene.render.engine == 'CYCLES' and hasattr(context.scene, 'cycles'):
2029 intensity = 1/context.scene.cycles.film_exposure # Film exposure is a multiplier
2030 else:
2031 intensity = 1
2033 intensity /= pow(2, (context.scene.view_settings.exposure)) # CM exposure is measured in stops/EVs (2^x)
2034 emission.inputs[1].default_value = intensity
2036 else:
2037 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2038 socket_type = 'NodeSocketShader'
2039 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
2040 make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
2041 output_socket = materialout.inputs[materialout_index]
2042 for node in base_node_tree.nodes:
2043 if node.name == 'Emission Viewer':
2044 delete_nodes.append((base_node_tree, node))
2045 for li_from, li_to in make_links:
2046 base_node_tree.links.new(li_from, li_to)
2048 # Create links through node groups until we reach the active node
2049 tree = base_node_tree
2050 link_end = output_socket
2051 while tree.nodes.active != active:
2052 node = tree.nodes.active
2053 index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
2054 link_start = node.outputs[index]
2055 node_socket = node.node_tree.outputs[index]
2056 if node_socket in delete_sockets:
2057 delete_sockets.remove(node_socket)
2058 tree.links.new(link_start, link_end)
2059 # Iterate
2060 link_end = self.ensure_group_output(node.node_tree).inputs[index]
2061 tree = tree.nodes.active.node_tree
2062 tree.links.new(active.outputs[out_i], link_end)
2064 # Delete sockets
2065 for socket in delete_sockets:
2066 if not self.is_socket_used_other_mats(socket):
2067 tree = socket.id_data
2068 tree.outputs.remove(socket)
2070 # Delete nodes
2071 for tree, node in delete_nodes:
2072 tree.nodes.remove(node)
2074 nodes.active = active
2075 active.select = True
2077 force_update(context)
2079 return {'FINISHED'}
2080 else:
2081 return {'CANCELLED'}
2084 class NWFrameSelected(Operator, NWBase):
2085 bl_idname = "node.nw_frame_selected"
2086 bl_label = "Frame Selected"
2087 bl_description = "Add a frame node and parent the selected nodes to it"
2088 bl_options = {'REGISTER', 'UNDO'}
2090 label_prop: StringProperty(
2091 name='Label',
2092 description='The visual name of the frame node',
2093 default=' '
2095 color_prop: FloatVectorProperty(
2096 name="Color",
2097 description="The color of the frame node",
2098 default=(0.6, 0.6, 0.6),
2099 min=0, max=1, step=1, precision=3,
2100 subtype='COLOR_GAMMA', size=3
2103 def execute(self, context):
2104 nodes, links = get_nodes_links(context)
2105 selected = []
2106 for node in nodes:
2107 if node.select == True:
2108 selected.append(node)
2110 bpy.ops.node.add_node(type='NodeFrame')
2111 frm = nodes.active
2112 frm.label = self.label_prop
2113 frm.use_custom_color = True
2114 frm.color = self.color_prop
2116 for node in selected:
2117 node.parent = frm
2119 return {'FINISHED'}
2122 class NWReloadImages(Operator):
2123 bl_idname = "node.nw_reload_images"
2124 bl_label = "Reload Images"
2125 bl_description = "Update all the image nodes to match their files on disk"
2127 @classmethod
2128 def poll(cls, context):
2129 valid = False
2130 if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
2131 if context.active_node is not None:
2132 for out in context.active_node.outputs:
2133 if is_visible_socket(out):
2134 valid = True
2135 break
2136 return valid
2138 def execute(self, context):
2139 nodes, links = get_nodes_links(context)
2140 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2141 num_reloaded = 0
2142 for node in nodes:
2143 if node.type in image_types:
2144 if node.type == "TEXTURE":
2145 if node.texture: # node has texture assigned
2146 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2147 if node.texture.image: # texture has image assigned
2148 node.texture.image.reload()
2149 num_reloaded += 1
2150 else:
2151 if node.image:
2152 node.image.reload()
2153 num_reloaded += 1
2155 if num_reloaded:
2156 self.report({'INFO'}, "Reloaded images")
2157 print("Reloaded " + str(num_reloaded) + " images")
2158 force_update(context)
2159 return {'FINISHED'}
2160 else:
2161 self.report({'WARNING'}, "No images found to reload in this node tree")
2162 return {'CANCELLED'}
2165 class NWSwitchNodeType(Operator, NWBase):
2166 """Switch type of selected nodes """
2167 bl_idname = "node.nw_swtch_node_type"
2168 bl_label = "Switch Node Type"
2169 bl_options = {'REGISTER', 'UNDO'}
2171 to_type: EnumProperty(
2172 name="Switch to type",
2173 items=list(shaders_input_nodes_props) +
2174 list(shaders_output_nodes_props) +
2175 list(shaders_shader_nodes_props) +
2176 list(shaders_texture_nodes_props) +
2177 list(shaders_color_nodes_props) +
2178 list(shaders_vector_nodes_props) +
2179 list(shaders_converter_nodes_props) +
2180 list(shaders_layout_nodes_props) +
2181 list(compo_input_nodes_props) +
2182 list(compo_output_nodes_props) +
2183 list(compo_color_nodes_props) +
2184 list(compo_converter_nodes_props) +
2185 list(compo_filter_nodes_props) +
2186 list(compo_vector_nodes_props) +
2187 list(compo_matte_nodes_props) +
2188 list(compo_distort_nodes_props) +
2189 list(compo_layout_nodes_props) +
2190 list(blender_mat_input_nodes_props) +
2191 list(blender_mat_output_nodes_props) +
2192 list(blender_mat_color_nodes_props) +
2193 list(blender_mat_vector_nodes_props) +
2194 list(blender_mat_converter_nodes_props) +
2195 list(blender_mat_layout_nodes_props) +
2196 list(texture_input_nodes_props) +
2197 list(texture_output_nodes_props) +
2198 list(texture_color_nodes_props) +
2199 list(texture_pattern_nodes_props) +
2200 list(texture_textures_nodes_props) +
2201 list(texture_converter_nodes_props) +
2202 list(texture_distort_nodes_props) +
2203 list(texture_layout_nodes_props)
2206 geo_to_type: StringProperty(
2207 name="Switch to type",
2208 default = '',
2211 def execute(self, context):
2212 nodes, links = get_nodes_links(context)
2213 to_type = self.to_type
2214 if self.geo_to_type != '':
2215 to_type = self.geo_to_type
2216 # Those types of nodes will not swap.
2217 src_excludes = ('NodeFrame')
2218 # Those attributes of nodes will be copied if possible
2219 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
2220 'show_options', 'show_preview', 'show_texture',
2221 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2223 selected = [n for n in nodes if n.select]
2224 reselect = []
2225 for node in [n for n in selected if
2226 n.rna_type.identifier not in src_excludes and
2227 n.rna_type.identifier != to_type]:
2228 new_node = nodes.new(to_type)
2229 for attr in attrs_to_pass:
2230 if hasattr(node, attr) and hasattr(new_node, attr):
2231 setattr(new_node, attr, getattr(node, attr))
2232 # set image datablock of dst to image of src
2233 if hasattr(node, 'image') and hasattr(new_node, 'image'):
2234 if node.image:
2235 new_node.image = node.image
2236 # Special cases
2237 if new_node.type == 'SWITCH':
2238 new_node.hide = True
2239 # Dictionaries: src_sockets and dst_sockets:
2240 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2241 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2242 # in 'INPUTS' and 'OUTPUTS':
2243 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2244 # socket entry:
2245 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2246 src_sockets = {
2247 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2248 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2250 dst_sockets = {
2251 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2252 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2254 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2255 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2256 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2257 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
2258 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2259 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
2260 # enumerate in inputs, then in outputs
2261 # find name, default value and links of socket
2262 for i, socket in enumerate(in_out):
2263 the_name = socket.name
2264 dval = None
2265 # Not every socket, especially in outputs has "default_value"
2266 if hasattr(socket, 'default_value'):
2267 dval = socket.default_value
2268 socket_links = []
2269 for lnk in socket.links:
2270 socket_links.append(lnk)
2271 # check type of socket to fill proper keys.
2272 for the_type in types_order_one:
2273 if socket.type == the_type:
2274 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2275 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2276 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
2277 # Check which of the types in inputs/outputs is considered to be "main".
2278 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2279 for type_check in types_order_one:
2280 if sockets[in_out_name][type_check]:
2281 sockets[in_out_name]['MAIN'] = type_check
2282 break
2284 matches = {
2285 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2286 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2289 for inout, soctype in (
2290 ('INPUTS', 'MAIN',),
2291 ('INPUTS', 'SHADER',),
2292 ('INPUTS', 'RGBA',),
2293 ('INPUTS', 'VECTOR',),
2294 ('INPUTS', 'VALUE',),
2295 ('OUTPUTS', 'MAIN',),
2296 ('OUTPUTS', 'SHADER',),
2297 ('OUTPUTS', 'RGBA',),
2298 ('OUTPUTS', 'VECTOR',),
2299 ('OUTPUTS', 'VALUE',),
2301 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
2302 if soctype == 'MAIN':
2303 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
2304 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
2305 else:
2306 sc = src_sockets[inout][soctype]
2307 dt = dst_sockets[inout][soctype]
2308 # start with 'dt' to determine number of possibilities.
2309 for i, soc in enumerate(dt):
2310 # if src main has enough entries - match them with dst main sockets by indexes.
2311 if len(sc) > i:
2312 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
2313 # add 'VALUE_NAME' criterion to inputs.
2314 if inout == 'INPUTS' and soctype == 'VALUE':
2315 for s in sc:
2316 if s[2] == soc[2]: # if names match
2317 # append src (index, dval), dst (index, dval)
2318 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
2320 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2321 # This creates better links when relinking textures.
2322 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
2323 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
2325 # Pass default values and RELINK:
2326 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2327 # INPUTS: Base on matches in proper order.
2328 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
2329 # pass dvals
2330 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
2331 new_node.inputs[dst_i].default_value = src_dval
2332 # Special case: switch to math
2333 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2334 new_node.type == 'MATH' and\
2335 tp == 'MAIN':
2336 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
2337 new_node.inputs[dst_i].default_value = new_dst_dval
2338 if node.type == 'MIX_RGB':
2339 if node.blend_type in [o[0] for o in operations]:
2340 new_node.operation = node.blend_type
2341 # Special case: switch from math to some types
2342 if node.type == 'MATH' and\
2343 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2344 tp == 'MAIN':
2345 for i in range(3):
2346 new_node.inputs[dst_i].default_value[i] = src_dval
2347 if new_node.type == 'MIX_RGB':
2348 if node.operation in [t[0] for t in blend_types]:
2349 new_node.blend_type = node.operation
2350 # Set Fac of MIX_RGB to 1.0
2351 new_node.inputs[0].default_value = 1.0
2352 # make link only when dst matching input is not linked already.
2353 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
2354 in_src_link = node.inputs[src_i].links[0]
2355 in_dst_socket = new_node.inputs[dst_i]
2356 links.new(in_src_link.from_socket, in_dst_socket)
2357 links.remove(in_src_link)
2358 # OUTPUTS: Base on matches in proper order.
2359 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
2360 for out_src_link in node.outputs[src_i].links:
2361 out_dst_socket = new_node.outputs[dst_i]
2362 links.new(out_dst_socket, out_src_link.to_socket)
2363 # relink rest inputs if possible, no criteria
2364 for src_inp in node.inputs:
2365 for dst_inp in new_node.inputs:
2366 if src_inp.links and not dst_inp.links:
2367 src_link = src_inp.links[0]
2368 links.new(src_link.from_socket, dst_inp)
2369 links.remove(src_link)
2370 # relink rest outputs if possible, base on node kind if any left.
2371 for src_o in node.outputs:
2372 for out_src_link in src_o.links:
2373 for dst_o in new_node.outputs:
2374 if src_o.type == dst_o.type:
2375 links.new(dst_o, out_src_link.to_socket)
2376 # relink rest outputs no criteria if any left. Link all from first output.
2377 for src_o in node.outputs:
2378 for out_src_link in src_o.links:
2379 if new_node.outputs:
2380 links.new(new_node.outputs[0], out_src_link.to_socket)
2381 nodes.remove(node)
2382 force_update(context)
2383 return {'FINISHED'}
2386 class NWMergeNodes(Operator, NWBase):
2387 bl_idname = "node.nw_merge_nodes"
2388 bl_label = "Merge Nodes"
2389 bl_description = "Merge Selected Nodes"
2390 bl_options = {'REGISTER', 'UNDO'}
2392 mode: EnumProperty(
2393 name="mode",
2394 description="All possible blend types, boolean operations and math operations",
2395 items= blend_types + [op for op in geo_combine_operations if op not in blend_types] + [op for op in operations if op not in blend_types],
2397 merge_type: EnumProperty(
2398 name="merge type",
2399 description="Type of Merge to be used",
2400 items=(
2401 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2402 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2403 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2404 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2405 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2406 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2407 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2411 # Check if the link connects to a node that is in selected_nodes
2412 # If not, then check recursively for each link in the nodes outputs.
2413 # If yes, return True. If the recursion stops without finding a node
2414 # in selected_nodes, it returns False. The depth is used to prevent
2415 # getting stuck in a loop because of an already present cycle.
2416 @staticmethod
2417 def link_creates_cycle(link, selected_nodes, depth=0)->bool:
2418 if depth > 255:
2419 # We're stuck in a cycle, but that cycle was already present,
2420 # so we return False.
2421 # NOTE: The number 255 is arbitrary, but seems to work well.
2422 return False
2423 node = link.to_node
2424 if node in selected_nodes:
2425 return True
2426 if not node.outputs:
2427 return False
2428 for output in node.outputs:
2429 if output.is_linked:
2430 for olink in output.links:
2431 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1):
2432 return True
2433 # None of the outputs found a node in selected_nodes, so there is no cycle.
2434 return False
2436 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2437 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2438 # be connected. The last one is assumed to be a multi input socket.
2439 # For convenience the node is returned.
2440 @staticmethod
2441 def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices):
2442 # The y-location of the last node
2443 loc_y = nodes_list[-1][2]
2444 if merge_position == 'CENTER':
2445 # Average the y-location
2446 for i in range(len(nodes_list)-1):
2447 loc_y += nodes_list[i][2]
2448 loc_y = loc_y/len(nodes_list)
2449 new_node = nodes.new(node_name)
2450 new_node.hide = do_hide
2451 new_node.location.x = loc_x
2452 new_node.location.y = loc_y
2453 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
2454 prev_links = []
2455 outputs_for_multi_input = []
2456 for i,node in enumerate(selected_nodes):
2457 node.select = False
2458 # Search for the first node which had output links that do not create
2459 # a cycle, which we can then reconnect afterwards.
2460 if prev_links == [] and node.outputs[0].is_linked:
2461 prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)]
2462 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2463 # To get the placement to look right we need to reverse the order in which we connect the
2464 # outputs to the multi input socket.
2465 if i < len(socket_indices) - 1:
2466 ind = socket_indices[i]
2467 links.new(node.outputs[0], new_node.inputs[ind])
2468 else:
2469 outputs_for_multi_input.insert(0, node.outputs[0])
2470 if outputs_for_multi_input != []:
2471 ind = socket_indices[-1]
2472 for output in outputs_for_multi_input:
2473 links.new(output, new_node.inputs[ind])
2474 if prev_links != []:
2475 for link in prev_links:
2476 links.new(new_node.outputs[0], link.to_node.inputs[0])
2477 return new_node
2479 def execute(self, context):
2480 settings = context.preferences.addons[__name__].preferences
2481 merge_hide = settings.merge_hide
2482 merge_position = settings.merge_position # 'center' or 'bottom'
2484 do_hide = False
2485 do_hide_shader = False
2486 if merge_hide == 'ALWAYS':
2487 do_hide = True
2488 do_hide_shader = True
2489 elif merge_hide == 'NON_SHADER':
2490 do_hide = True
2492 tree_type = context.space_data.node_tree.type
2493 if tree_type == 'GEOMETRY':
2494 node_type = 'GeometryNode'
2495 if tree_type == 'COMPOSITING':
2496 node_type = 'CompositorNode'
2497 elif tree_type == 'SHADER':
2498 node_type = 'ShaderNode'
2499 elif tree_type == 'TEXTURE':
2500 node_type = 'TextureNode'
2501 nodes, links = get_nodes_links(context)
2502 mode = self.mode
2503 merge_type = self.merge_type
2504 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2505 # 'ZCOMBINE' works only if mode == 'MIX'
2506 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2507 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2508 merge_type = 'MIX'
2509 mode = 'MIX'
2510 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
2511 merge_type = 'AUTO'
2512 # The math nodes used for geometry nodes are of type 'ShaderNode'
2513 if merge_type == 'MATH' and tree_type == 'GEOMETRY':
2514 node_type = 'ShaderNode'
2515 selected_mix = [] # entry = [index, loc]
2516 selected_shader = [] # entry = [index, loc]
2517 selected_geometry = [] # entry = [index, loc]
2518 selected_math = [] # entry = [index, loc]
2519 selected_vector = [] # entry = [index, loc]
2520 selected_z = [] # entry = [index, loc]
2521 selected_alphaover = [] # entry = [index, loc]
2523 for i, node in enumerate(nodes):
2524 if node.select and node.outputs:
2525 if merge_type == 'AUTO':
2526 for (type, types_list, dst) in (
2527 ('SHADER', ('MIX', 'ADD'), selected_shader),
2528 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2529 ('RGBA', [t[0] for t in blend_types], selected_mix),
2530 ('VALUE', [t[0] for t in operations], selected_math),
2531 ('VECTOR', [], selected_vector),
2533 output_type = node.outputs[0].type
2534 valid_mode = mode in types_list
2535 # When mode is 'MIX' we have to cheat since the mix node is not used in
2536 # geometry nodes.
2537 if tree_type == 'GEOMETRY':
2538 if mode == 'MIX':
2539 if output_type == 'VALUE' and type == 'VALUE':
2540 valid_mode = True
2541 elif output_type == 'VECTOR' and type == 'VECTOR':
2542 valid_mode = True
2543 elif type == 'GEOMETRY':
2544 valid_mode = True
2545 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2546 # Cheat that output type is 'RGBA',
2547 # and that 'MIX' exists in math operations list.
2548 # This way when selected_mix list is analyzed:
2549 # Node data will be appended even though it doesn't meet requirements.
2550 elif output_type != 'SHADER' and mode == 'MIX':
2551 output_type = 'RGBA'
2552 valid_mode = True
2553 if output_type == type and valid_mode:
2554 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2555 else:
2556 for (type, types_list, dst) in (
2557 ('SHADER', ('MIX', 'ADD'), selected_shader),
2558 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2559 ('MIX', [t[0] for t in blend_types], selected_mix),
2560 ('MATH', [t[0] for t in operations], selected_math),
2561 ('ZCOMBINE', ('MIX', ), selected_z),
2562 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2564 if merge_type == type and mode in types_list:
2565 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2566 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2567 # use only 'Mix' nodes for merging.
2568 # For that we add selected_math list to selected_mix list and clear selected_math.
2569 if selected_mix and selected_math and merge_type == 'AUTO':
2570 selected_mix += selected_math
2571 selected_math = []
2572 for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]:
2573 if not nodes_list:
2574 continue
2575 count_before = len(nodes)
2576 # sort list by loc_x - reversed
2577 nodes_list.sort(key=lambda k: k[1], reverse=True)
2578 # get maximum loc_x
2579 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2580 nodes_list.sort(key=lambda k: k[2], reverse=True)
2582 # Change the node type for math nodes in a geometry node tree.
2583 if tree_type == 'GEOMETRY':
2584 if nodes_list is selected_math or nodes_list is selected_vector:
2585 node_type = 'ShaderNode'
2586 if mode == 'MIX':
2587 mode = 'ADD'
2588 else:
2589 node_type = 'GeometryNode'
2590 if merge_position == 'CENTER':
2591 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)
2592 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2593 if do_hide:
2594 loc_y += 40
2595 else:
2596 loc_y += 80
2597 else:
2598 loc_y = nodes_list[len(nodes_list) - 1][2]
2599 offset_y = 100
2600 if not do_hide:
2601 offset_y = 200
2602 if nodes_list == selected_shader and not do_hide_shader:
2603 offset_y = 150.0
2604 the_range = len(nodes_list) - 1
2605 if len(nodes_list) == 1:
2606 the_range = 1
2607 was_multi = False
2608 for i in range(the_range):
2609 if nodes_list == selected_mix:
2610 add_type = node_type + 'MixRGB'
2611 add = nodes.new(add_type)
2612 add.blend_type = mode
2613 if mode != 'MIX':
2614 add.inputs[0].default_value = 1.0
2615 add.show_preview = False
2616 add.hide = do_hide
2617 if do_hide:
2618 loc_y = loc_y - 50
2619 first = 1
2620 second = 2
2621 add.width_hidden = 100.0
2622 elif nodes_list == selected_math:
2623 add_type = node_type + 'Math'
2624 add = nodes.new(add_type)
2625 add.operation = mode
2626 add.hide = do_hide
2627 if do_hide:
2628 loc_y = loc_y - 50
2629 first = 0
2630 second = 1
2631 add.width_hidden = 100.0
2632 elif nodes_list == selected_shader:
2633 if mode == 'MIX':
2634 add_type = node_type + 'MixShader'
2635 add = nodes.new(add_type)
2636 add.hide = do_hide_shader
2637 if do_hide_shader:
2638 loc_y = loc_y - 50
2639 first = 1
2640 second = 2
2641 add.width_hidden = 100.0
2642 elif mode == 'ADD':
2643 add_type = node_type + 'AddShader'
2644 add = nodes.new(add_type)
2645 add.hide = do_hide_shader
2646 if do_hide_shader:
2647 loc_y = loc_y - 50
2648 first = 0
2649 second = 1
2650 add.width_hidden = 100.0
2651 elif nodes_list == selected_geometry:
2652 if mode in ('JOIN', 'MIX'):
2653 add_type = node_type + 'JoinGeometry'
2654 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0])
2655 else:
2656 add_type = node_type + 'Boolean'
2657 indices = [0,1] if mode == 'DIFFERENCE' else [1]
2658 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices)
2659 add.operation = mode
2660 was_multi = True
2661 break
2662 elif nodes_list == selected_vector:
2663 add_type = node_type + 'VectorMath'
2664 add = nodes.new(add_type)
2665 add.operation = mode
2666 add.hide = do_hide
2667 if do_hide:
2668 loc_y = loc_y - 50
2669 first = 0
2670 second = 1
2671 add.width_hidden = 100.0
2672 elif nodes_list == selected_z:
2673 add = nodes.new('CompositorNodeZcombine')
2674 add.show_preview = False
2675 add.hide = do_hide
2676 if do_hide:
2677 loc_y = loc_y - 50
2678 first = 0
2679 second = 2
2680 add.width_hidden = 100.0
2681 elif nodes_list == selected_alphaover:
2682 add = nodes.new('CompositorNodeAlphaOver')
2683 add.show_preview = False
2684 add.hide = do_hide
2685 if do_hide:
2686 loc_y = loc_y - 50
2687 first = 1
2688 second = 2
2689 add.width_hidden = 100.0
2690 add.location = loc_x, loc_y
2691 loc_y += offset_y
2692 add.select = True
2694 # This has already been handled separately
2695 if was_multi:
2696 continue
2697 count_adds = i + 1
2698 count_after = len(nodes)
2699 index = count_after - 1
2700 first_selected = nodes[nodes_list[0][0]]
2701 # "last" node has been added as first, so its index is count_before.
2702 last_add = nodes[count_before]
2703 # Create list of invalid indexes.
2704 invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
2706 # Special case:
2707 # Two nodes were selected and first selected has no output links, second selected has output links.
2708 # Then add links from last add to all links 'to_socket' of out links of second selected.
2709 if len(nodes_list) == 2:
2710 if not first_selected.outputs[0].links:
2711 second_selected = nodes[nodes_list[1][0]]
2712 for ss_link in second_selected.outputs[0].links:
2713 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2714 # Link only if "to_node" index not in invalid indexes list.
2715 if not self.link_creates_cycle(ss_link, invalid_nodes):
2716 links.new(last_add.outputs[0], ss_link.to_socket)
2717 # add links from last_add to all links 'to_socket' of out links of first selected.
2718 for fs_link in first_selected.outputs[0].links:
2719 # Link only if "to_node" index not in invalid indexes list.
2720 if not self.link_creates_cycle(fs_link, invalid_nodes):
2721 links.new(last_add.outputs[0], fs_link.to_socket)
2722 # add link from "first" selected and "first" add node
2723 node_to = nodes[count_after - 1]
2724 links.new(first_selected.outputs[0], node_to.inputs[first])
2725 if node_to.type == 'ZCOMBINE':
2726 for fs_out in first_selected.outputs:
2727 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
2728 links.new(fs_out, node_to.inputs[1])
2729 break
2730 # add links between added ADD nodes and between selected and ADD nodes
2731 for i in range(count_adds):
2732 if i < count_adds - 1:
2733 node_from = nodes[index]
2734 node_to = nodes[index - 1]
2735 node_to_input_i = first
2736 node_to_z_i = 1 # if z combine - link z to first z input
2737 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2738 if node_to.type == 'ZCOMBINE':
2739 for from_out in node_from.outputs:
2740 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2741 links.new(from_out, node_to.inputs[node_to_z_i])
2742 if len(nodes_list) > 1:
2743 node_from = nodes[nodes_list[i + 1][0]]
2744 node_to = nodes[index]
2745 node_to_input_i = second
2746 node_to_z_i = 3 # if z combine - link z to second z input
2747 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2748 if node_to.type == 'ZCOMBINE':
2749 for from_out in node_from.outputs:
2750 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2751 links.new(from_out, node_to.inputs[node_to_z_i])
2752 index -= 1
2753 # set "last" of added nodes as active
2754 nodes.active = last_add
2755 for i, x, y, dx, h in nodes_list:
2756 nodes[i].select = False
2758 return {'FINISHED'}
2761 class NWBatchChangeNodes(Operator, NWBase):
2762 bl_idname = "node.nw_batch_change"
2763 bl_label = "Batch Change"
2764 bl_description = "Batch Change Blend Type and Math Operation"
2765 bl_options = {'REGISTER', 'UNDO'}
2767 blend_type: EnumProperty(
2768 name="Blend Type",
2769 items=blend_types + navs,
2771 operation: EnumProperty(
2772 name="Operation",
2773 items=operations + navs,
2776 def execute(self, context):
2777 blend_type = self.blend_type
2778 operation = self.operation
2779 for node in context.selected_nodes:
2780 if node.type == 'MIX_RGB' or node.bl_idname == 'GeometryNodeAttributeMix':
2781 if not blend_type in [nav[0] for nav in navs]:
2782 node.blend_type = blend_type
2783 else:
2784 if blend_type == 'NEXT':
2785 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2786 #index = blend_types.index(node.blend_type)
2787 if index == len(blend_types) - 1:
2788 node.blend_type = blend_types[0][0]
2789 else:
2790 node.blend_type = blend_types[index + 1][0]
2792 if blend_type == 'PREV':
2793 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2794 if index == 0:
2795 node.blend_type = blend_types[len(blend_types) - 1][0]
2796 else:
2797 node.blend_type = blend_types[index - 1][0]
2799 if node.type == 'MATH' or node.bl_idname == 'GeometryNodeAttributeMath':
2800 if not operation in [nav[0] for nav in navs]:
2801 node.operation = operation
2802 else:
2803 if operation == 'NEXT':
2804 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2805 #index = operations.index(node.operation)
2806 if index == len(operations) - 1:
2807 node.operation = operations[0][0]
2808 else:
2809 node.operation = operations[index + 1][0]
2811 if operation == 'PREV':
2812 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2813 #index = operations.index(node.operation)
2814 if index == 0:
2815 node.operation = operations[len(operations) - 1][0]
2816 else:
2817 node.operation = operations[index - 1][0]
2819 return {'FINISHED'}
2822 class NWChangeMixFactor(Operator, NWBase):
2823 bl_idname = "node.nw_factor"
2824 bl_label = "Change Factor"
2825 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2826 bl_options = {'REGISTER', 'UNDO'}
2828 # option: Change factor.
2829 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2830 # Else - change factor by option value.
2831 option: FloatProperty()
2833 def execute(self, context):
2834 nodes, links = get_nodes_links(context)
2835 option = self.option
2836 selected = [] # entry = index
2837 for si, node in enumerate(nodes):
2838 if node.select:
2839 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2840 selected.append(si)
2842 for si in selected:
2843 fac = nodes[si].inputs[0]
2844 nodes[si].hide = False
2845 if option in {0.0, 1.0}:
2846 fac.default_value = option
2847 else:
2848 fac.default_value += option
2850 return {'FINISHED'}
2853 class NWCopySettings(Operator, NWBase):
2854 bl_idname = "node.nw_copy_settings"
2855 bl_label = "Copy Settings"
2856 bl_description = "Copy Settings of Active Node to Selected Nodes"
2857 bl_options = {'REGISTER', 'UNDO'}
2859 @classmethod
2860 def poll(cls, context):
2861 valid = False
2862 if nw_check(context):
2863 if (
2864 context.active_node is not None and
2865 context.active_node.type != 'FRAME'
2867 valid = True
2868 return valid
2870 def execute(self, context):
2871 node_active = context.active_node
2872 node_selected = context.selected_nodes
2874 # Error handling
2875 if not (len(node_selected) > 1):
2876 self.report({'ERROR'}, "2 nodes must be selected at least")
2877 return {'CANCELLED'}
2879 # Check if active node is in the selection
2880 selected_node_names = [n.name for n in node_selected]
2881 if node_active.name not in selected_node_names:
2882 self.report({'ERROR'}, "No active node")
2883 return {'CANCELLED'}
2885 # Get nodes in selection by type
2886 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2888 if not (len(valid_nodes) > 1) and node_active:
2889 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2890 return {'CANCELLED'}
2892 if len(valid_nodes) != len(node_selected):
2893 # Report nodes that are not valid
2894 valid_node_names = [n.name for n in valid_nodes]
2895 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2896 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2898 # Reference original
2899 orig = node_active
2900 #node_selected_names = [n.name for n in node_selected]
2902 # Output list
2903 success_names = []
2905 # Deselect all nodes
2906 for i in node_selected:
2907 i.select = False
2909 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2910 # Run through all other nodes
2911 for node in valid_nodes[1:]:
2913 # Check for frame node
2914 parent = node.parent if node.parent else None
2915 node_loc = [node.location.x, node.location.y]
2917 # Select original to duplicate
2918 orig.select = True
2920 # Duplicate selected node
2921 bpy.ops.node.duplicate()
2922 new_node = context.selected_nodes[0]
2924 # Deselect copy
2925 new_node.select = False
2927 # Properties to copy
2928 node_tree = node.id_data
2929 props_to_copy = 'bl_idname name location height width'.split(' ')
2931 # Input and outputs
2932 reconnections = []
2933 mappings = chain.from_iterable([node.inputs, node.outputs])
2934 for i in (i for i in mappings if i.is_linked):
2935 for L in i.links:
2936 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2938 # Properties
2939 props = {j: getattr(node, j) for j in props_to_copy}
2940 props_to_copy.pop(0)
2942 for prop in props_to_copy:
2943 setattr(new_node, prop, props[prop])
2945 # Get the node tree to remove the old node
2946 nodes = node_tree.nodes
2947 nodes.remove(node)
2948 new_node.name = props['name']
2950 if parent:
2951 new_node.parent = parent
2952 new_node.location = node_loc
2954 for str_from, str_to in reconnections:
2955 node_tree.links.new(eval(str_from), eval(str_to))
2957 success_names.append(new_node.name)
2959 orig.select = True
2960 node_tree.nodes.active = orig
2961 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2962 return {'FINISHED'}
2965 class NWCopyLabel(Operator, NWBase):
2966 bl_idname = "node.nw_copy_label"
2967 bl_label = "Copy Label"
2968 bl_options = {'REGISTER', 'UNDO'}
2970 option: EnumProperty(
2971 name="option",
2972 description="Source of name of label",
2973 items=(
2974 ('FROM_ACTIVE', 'from active', 'from active node',),
2975 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2976 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2980 def execute(self, context):
2981 nodes, links = get_nodes_links(context)
2982 option = self.option
2983 active = nodes.active
2984 if option == 'FROM_ACTIVE':
2985 if active:
2986 src_label = active.label
2987 for node in [n for n in nodes if n.select and nodes.active != n]:
2988 node.label = src_label
2989 elif option == 'FROM_NODE':
2990 selected = [n for n in nodes if n.select]
2991 for node in selected:
2992 for input in node.inputs:
2993 if input.links:
2994 src = input.links[0].from_node
2995 node.label = src.label
2996 break
2997 elif option == 'FROM_SOCKET':
2998 selected = [n for n in nodes if n.select]
2999 for node in selected:
3000 for input in node.inputs:
3001 if input.links:
3002 src = input.links[0].from_socket
3003 node.label = src.name
3004 break
3006 return {'FINISHED'}
3009 class NWClearLabel(Operator, NWBase):
3010 bl_idname = "node.nw_clear_label"
3011 bl_label = "Clear Label"
3012 bl_options = {'REGISTER', 'UNDO'}
3014 option: BoolProperty()
3016 def execute(self, context):
3017 nodes, links = get_nodes_links(context)
3018 for node in [n for n in nodes if n.select]:
3019 node.label = ''
3021 return {'FINISHED'}
3023 def invoke(self, context, event):
3024 if self.option:
3025 return self.execute(context)
3026 else:
3027 return context.window_manager.invoke_confirm(self, event)
3030 class NWModifyLabels(Operator, NWBase):
3031 """Modify Labels of all selected nodes"""
3032 bl_idname = "node.nw_modify_labels"
3033 bl_label = "Modify Labels"
3034 bl_options = {'REGISTER', 'UNDO'}
3036 prepend: StringProperty(
3037 name="Add to Beginning"
3039 append: StringProperty(
3040 name="Add to End"
3042 replace_from: StringProperty(
3043 name="Text to Replace"
3045 replace_to: StringProperty(
3046 name="Replace with"
3049 def execute(self, context):
3050 nodes, links = get_nodes_links(context)
3051 for node in [n for n in nodes if n.select]:
3052 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
3054 return {'FINISHED'}
3056 def invoke(self, context, event):
3057 self.prepend = ""
3058 self.append = ""
3059 self.remove = ""
3060 return context.window_manager.invoke_props_dialog(self)
3063 class NWAddTextureSetup(Operator, NWBase):
3064 bl_idname = "node.nw_add_texture"
3065 bl_label = "Texture Setup"
3066 bl_description = "Add Texture Node Setup to Selected Shaders"
3067 bl_options = {'REGISTER', 'UNDO'}
3069 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
3071 @classmethod
3072 def poll(cls, context):
3073 valid = False
3074 if nw_check(context):
3075 space = context.space_data
3076 if space.tree_type == 'ShaderNodeTree':
3077 valid = True
3078 return valid
3080 def execute(self, context):
3081 nodes, links = get_nodes_links(context)
3082 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3083 texture_types = [x[1] for x in shaders_texture_nodes_props]
3084 selected_nodes = [n for n in nodes if n.select]
3085 for t_node in selected_nodes:
3086 valid = False
3087 input_index = 0
3088 if t_node.inputs:
3089 for index, i in enumerate(t_node.inputs):
3090 if not i.is_linked:
3091 valid = True
3092 input_index = index
3093 break
3094 if valid:
3095 locx = t_node.location.x
3096 locy = t_node.location.y - t_node.dimensions.y/2
3098 xoffset = [500, 700]
3099 is_texture = False
3100 if t_node.type in texture_types + ['MAPPING']:
3101 xoffset = [290, 500]
3102 is_texture = True
3104 coordout = 2
3105 image_type = 'ShaderNodeTexImage'
3107 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
3108 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
3109 if t_node.type == 'BACKGROUND':
3110 image_type = 'ShaderNodeTexEnvironment'
3112 if not is_texture:
3113 tex = nodes.new(image_type)
3114 tex.location = [locx - 200, locy + 112]
3115 nodes.active = tex
3116 links.new(tex.outputs[0], t_node.inputs[input_index])
3118 t_node.select = False
3119 if self.add_mapping or is_texture:
3120 if t_node.type != 'MAPPING':
3121 m = nodes.new('ShaderNodeMapping')
3122 m.location = [locx - xoffset[0], locy + 141]
3123 m.width = 240
3124 else:
3125 m = t_node
3126 coord = nodes.new('ShaderNodeTexCoord')
3127 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
3129 if not is_texture:
3130 links.new(m.outputs[0], tex.inputs[0])
3131 links.new(coord.outputs[coordout], m.inputs[0])
3132 else:
3133 nodes.active = m
3134 links.new(m.outputs[0], t_node.inputs[input_index])
3135 links.new(coord.outputs[coordout], m.inputs[0])
3136 else:
3137 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
3138 return {'FINISHED'}
3141 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
3142 bl_idname = "node.nw_add_textures_for_principled"
3143 bl_label = "Principled Texture Setup"
3144 bl_description = "Add Texture Node Setup for Principled BSDF"
3145 bl_options = {'REGISTER', 'UNDO'}
3147 directory: StringProperty(
3148 name='Directory',
3149 subtype='DIR_PATH',
3150 default='',
3151 description='Folder to search in for image files'
3153 files: CollectionProperty(
3154 type=bpy.types.OperatorFileListElement,
3155 options={'HIDDEN', 'SKIP_SAVE'}
3158 relative_path: BoolProperty(
3159 name='Relative Path',
3160 description='Set the file path relative to the blend file, when possible',
3161 default=True
3164 order = [
3165 "filepath",
3166 "files",
3169 def draw(self, context):
3170 layout = self.layout
3171 layout.alignment = 'LEFT'
3173 layout.prop(self, 'relative_path')
3175 @classmethod
3176 def poll(cls, context):
3177 valid = False
3178 if nw_check(context):
3179 space = context.space_data
3180 if space.tree_type == 'ShaderNodeTree':
3181 valid = True
3182 return valid
3184 def execute(self, context):
3185 # Check if everything is ok
3186 if not self.directory:
3187 self.report({'INFO'}, 'No Folder Selected')
3188 return {'CANCELLED'}
3189 if not self.files[:]:
3190 self.report({'INFO'}, 'No Files Selected')
3191 return {'CANCELLED'}
3193 nodes, links = get_nodes_links(context)
3194 active_node = nodes.active
3195 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
3196 self.report({'INFO'}, 'Select Principled BSDF')
3197 return {'CANCELLED'}
3199 # Helper_functions
3200 def split_into__components(fname):
3201 # Split filename into components
3202 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3203 # Remove extension
3204 fname = path.splitext(fname)[0]
3205 # Remove digits
3206 fname = ''.join(i for i in fname if not i.isdigit())
3207 # Separate CamelCase by space
3208 fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",fname)
3209 # Replace common separators with SPACE
3210 separators = ['_', '.', '-', '__', '--', '#']
3211 for sep in separators:
3212 fname = fname.replace(sep, ' ')
3214 components = fname.split(' ')
3215 components = [c.lower() for c in components]
3216 return components
3218 # Filter textures names for texturetypes in filenames
3219 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3220 tags = context.preferences.addons[__name__].preferences.principled_tags
3221 normal_abbr = tags.normal.split(' ')
3222 bump_abbr = tags.bump.split(' ')
3223 gloss_abbr = tags.gloss.split(' ')
3224 rough_abbr = tags.rough.split(' ')
3225 socketnames = [
3226 ['Displacement', tags.displacement.split(' '), None],
3227 ['Base Color', tags.base_color.split(' '), None],
3228 ['Subsurface Color', tags.sss_color.split(' '), None],
3229 ['Metallic', tags.metallic.split(' '), None],
3230 ['Specular', tags.specular.split(' '), None],
3231 ['Roughness', rough_abbr + gloss_abbr, None],
3232 ['Normal', normal_abbr + bump_abbr, None],
3235 # Look through texture_types and set value as filename of first matched file
3236 def match_files_to_socket_names():
3237 for sname in socketnames:
3238 for file in self.files:
3239 fname = file.name
3240 filenamecomponents = split_into__components(fname)
3241 matches = set(sname[1]).intersection(set(filenamecomponents))
3242 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3243 if matches:
3244 sname[2] = fname
3245 break
3247 match_files_to_socket_names()
3248 # Remove socketnames without found files
3249 socketnames = [s for s in socketnames if s[2]
3250 and path.exists(self.directory+s[2])]
3251 if not socketnames:
3252 self.report({'INFO'}, 'No matching images found')
3253 print('No matching images found')
3254 return {'CANCELLED'}
3256 # Don't override path earlier as os.path is used to check the absolute path
3257 import_path = self.directory
3258 if self.relative_path:
3259 if bpy.data.filepath:
3260 try:
3261 import_path = bpy.path.relpath(self.directory)
3262 except ValueError:
3263 pass
3265 # Add found images
3266 print('\nMatched Textures:')
3267 texture_nodes = []
3268 disp_texture = None
3269 normal_node = None
3270 roughness_node = None
3271 for i, sname in enumerate(socketnames):
3272 print(i, sname[0], sname[2])
3274 # DISPLACEMENT NODES
3275 if sname[0] == 'Displacement':
3276 disp_texture = nodes.new(type='ShaderNodeTexImage')
3277 img = bpy.data.images.load(path.join(import_path, sname[2]))
3278 disp_texture.image = img
3279 disp_texture.label = 'Displacement'
3280 if disp_texture.image:
3281 disp_texture.image.colorspace_settings.is_data = True
3283 # Add displacement offset nodes
3284 disp_node = nodes.new(type='ShaderNodeDisplacement')
3285 disp_node.location = active_node.location + Vector((0, -560))
3286 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
3288 # TODO Turn on true displacement in the material
3289 # Too complicated for now
3291 # Find output node
3292 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
3293 if output_node:
3294 if not output_node[0].inputs[2].is_linked:
3295 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
3297 continue
3299 if not active_node.inputs[sname[0]].is_linked:
3300 # No texture node connected -> add texture node with new image
3301 texture_node = nodes.new(type='ShaderNodeTexImage')
3302 img = bpy.data.images.load(path.join(import_path, sname[2]))
3303 texture_node.image = img
3305 # NORMAL NODES
3306 if sname[0] == 'Normal':
3307 # Test if new texture node is normal or bump map
3308 fname_components = split_into__components(sname[2])
3309 match_normal = set(normal_abbr).intersection(set(fname_components))
3310 match_bump = set(bump_abbr).intersection(set(fname_components))
3311 if match_normal:
3312 # If Normal add normal node in between
3313 normal_node = nodes.new(type='ShaderNodeNormalMap')
3314 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
3315 elif match_bump:
3316 # If Bump add bump node in between
3317 normal_node = nodes.new(type='ShaderNodeBump')
3318 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
3320 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
3321 normal_node_texture = texture_node
3323 elif sname[0] == 'Roughness':
3324 # Test if glossy or roughness map
3325 fname_components = split_into__components(sname[2])
3326 match_rough = set(rough_abbr).intersection(set(fname_components))
3327 match_gloss = set(gloss_abbr).intersection(set(fname_components))
3329 if match_rough:
3330 # If Roughness nothing to to
3331 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
3333 elif match_gloss:
3334 # If Gloss Map add invert node
3335 invert_node = nodes.new(type='ShaderNodeInvert')
3336 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
3338 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
3339 roughness_node = texture_node
3341 else:
3342 # This is a simple connection Texture --> Input slot
3343 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
3345 # Use non-color for all but 'Base Color' Textures
3346 if not sname[0] in ['Base Color'] and texture_node.image:
3347 texture_node.image.colorspace_settings.is_data = True
3349 else:
3350 # If already texture connected. add to node list for alignment
3351 texture_node = active_node.inputs[sname[0]].links[0].from_node
3353 # This are all connected texture nodes
3354 texture_nodes.append(texture_node)
3355 texture_node.label = sname[0]
3357 if disp_texture:
3358 texture_nodes.append(disp_texture)
3360 # Alignment
3361 for i, texture_node in enumerate(texture_nodes):
3362 offset = Vector((-550, (i * -280) + 200))
3363 texture_node.location = active_node.location + offset
3365 if normal_node:
3366 # Extra alignment if normal node was added
3367 normal_node.location = normal_node_texture.location + Vector((300, 0))
3369 if roughness_node:
3370 # Alignment of invert node if glossy map
3371 invert_node.location = roughness_node.location + Vector((300, 0))
3373 # Add texture input + mapping
3374 mapping = nodes.new(type='ShaderNodeMapping')
3375 mapping.location = active_node.location + Vector((-1050, 0))
3376 if len(texture_nodes) > 1:
3377 # If more than one texture add reroute node in between
3378 reroute = nodes.new(type='NodeReroute')
3379 texture_nodes.append(reroute)
3380 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
3381 reroute.location = tex_coords + Vector((-50, -120))
3382 for texture_node in texture_nodes:
3383 link = links.new(texture_node.inputs[0], reroute.outputs[0])
3384 link = links.new(reroute.inputs[0], mapping.outputs[0])
3385 else:
3386 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
3388 # Connect texture_coordiantes to mapping node
3389 texture_input = nodes.new(type='ShaderNodeTexCoord')
3390 texture_input.location = mapping.location + Vector((-200, 0))
3391 link = links.new(mapping.inputs[0], texture_input.outputs[2])
3393 # Create frame around tex coords and mapping
3394 frame = nodes.new(type='NodeFrame')
3395 frame.label = 'Mapping'
3396 mapping.parent = frame
3397 texture_input.parent = frame
3398 frame.update()
3400 # Create frame around texture nodes
3401 frame = nodes.new(type='NodeFrame')
3402 frame.label = 'Textures'
3403 for tnode in texture_nodes:
3404 tnode.parent = frame
3405 frame.update()
3407 # Just to be sure
3408 active_node.select = False
3409 nodes.update()
3410 links.update()
3411 force_update(context)
3412 return {'FINISHED'}
3415 class NWAddReroutes(Operator, NWBase):
3416 """Add Reroute Nodes and link them to outputs of selected nodes"""
3417 bl_idname = "node.nw_add_reroutes"
3418 bl_label = "Add Reroutes"
3419 bl_description = "Add Reroutes to Outputs"
3420 bl_options = {'REGISTER', 'UNDO'}
3422 option: EnumProperty(
3423 name="option",
3424 items=[
3425 ('ALL', 'to all', 'Add to all outputs'),
3426 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3427 ('LINKED', 'to linked', 'Add only to linked outputs'),
3431 def execute(self, context):
3432 tree_type = context.space_data.node_tree.type
3433 option = self.option
3434 nodes, links = get_nodes_links(context)
3435 # output valid when option is 'all' or when 'loose' output has no links
3436 valid = False
3437 post_select = [] # nodes to be selected after execution
3438 # create reroutes and recreate links
3439 for node in [n for n in nodes if n.select]:
3440 if node.outputs:
3441 x = node.location.x
3442 y = node.location.y
3443 width = node.width
3444 # unhide 'REROUTE' nodes to avoid issues with location.y
3445 if node.type == 'REROUTE':
3446 node.hide = False
3447 # When node is hidden - width_hidden not usable.
3448 # Hack needed to calculate real width
3449 if node.hide:
3450 bpy.ops.node.select_all(action='DESELECT')
3451 helper = nodes.new('NodeReroute')
3452 helper.select = True
3453 node.select = True
3454 # resize node and helper to zero. Then check locations to calculate width
3455 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
3456 width = 2.0 * (helper.location.x - node.location.x)
3457 # restore node location
3458 node.location = x, y
3459 # delete helper
3460 node.select = False
3461 # only helper is selected now
3462 bpy.ops.node.delete()
3463 x = node.location.x + width + 20.0
3464 if node.type != 'REROUTE':
3465 y -= 35.0
3466 y_offset = -22.0
3467 loc = x, y
3468 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
3469 for out_i, output in enumerate(node.outputs):
3470 pass_used = False # initial value to be analyzed if 'R_LAYERS'
3471 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3472 if node.type != 'R_LAYERS':
3473 pass_used = True
3474 else: # if 'R_LAYERS' check if output represent used render pass
3475 node_scene = node.scene
3476 node_layer = node.layer
3477 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3478 if output.name == 'Alpha':
3479 pass_used = True
3480 else:
3481 # check entries in global 'rl_outputs' variable
3482 for rlo in rl_outputs:
3483 if output.name in {rlo.output_name, rlo.exr_output_name}:
3484 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
3485 break
3486 if pass_used:
3487 valid = ((option == 'ALL') or
3488 (option == 'LOOSE' and not output.links) or
3489 (option == 'LINKED' and output.links))
3490 # Add reroutes only if valid, but offset location in all cases.
3491 if valid:
3492 n = nodes.new('NodeReroute')
3493 nodes.active = n
3494 for link in output.links:
3495 links.new(n.outputs[0], link.to_socket)
3496 links.new(output, n.inputs[0])
3497 n.location = loc
3498 post_select.append(n)
3499 reroutes_count += 1
3500 y += y_offset
3501 loc = x, y
3502 # disselect the node so that after execution of script only newly created nodes are selected
3503 node.select = False
3504 # nicer reroutes distribution along y when node.hide
3505 if node.hide:
3506 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
3507 for reroute in [r for r in nodes if r.select]:
3508 reroute.location.y -= y_translate
3509 for node in post_select:
3510 node.select = True
3512 return {'FINISHED'}
3515 class NWLinkActiveToSelected(Operator, NWBase):
3516 """Link active node to selected nodes basing on various criteria"""
3517 bl_idname = "node.nw_link_active_to_selected"
3518 bl_label = "Link Active Node to Selected"
3519 bl_options = {'REGISTER', 'UNDO'}
3521 replace: BoolProperty()
3522 use_node_name: BoolProperty()
3523 use_outputs_names: BoolProperty()
3525 @classmethod
3526 def poll(cls, context):
3527 valid = False
3528 if nw_check(context):
3529 if context.active_node is not None:
3530 if context.active_node.select:
3531 valid = True
3532 return valid
3534 def execute(self, context):
3535 nodes, links = get_nodes_links(context)
3536 replace = self.replace
3537 use_node_name = self.use_node_name
3538 use_outputs_names = self.use_outputs_names
3539 active = nodes.active
3540 selected = [node for node in nodes if node.select and node != active]
3541 outputs = [] # Only usable outputs of active nodes will be stored here.
3542 for out in active.outputs:
3543 if active.type != 'R_LAYERS':
3544 outputs.append(out)
3545 else:
3546 # 'R_LAYERS' node type needs special handling.
3547 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3548 # Only outputs that represent used passes should be taken into account
3549 # Check if pass represented by output is used.
3550 # global 'rl_outputs' list will be used for that
3551 for rlo in rl_outputs:
3552 pass_used = False # initial value. Will be set to True if pass is used
3553 if out.name == 'Alpha':
3554 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3555 pass_used = True
3556 elif out.name in {rlo.output_name, rlo.exr_output_name}:
3557 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3558 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
3559 break
3560 if pass_used:
3561 outputs.append(out)
3562 doit = True # Will be changed to False when links successfully added to previous output.
3563 for out in outputs:
3564 if doit:
3565 for node in selected:
3566 dst_name = node.name # Will be compared with src_name if needed.
3567 # When node has label - use it as dst_name
3568 if node.label:
3569 dst_name = node.label
3570 valid = True # Initial value. Will be changed to False if names don't match.
3571 src_name = dst_name # If names not used - this assignment will keep valid = True.
3572 if use_node_name:
3573 # Set src_name to source node name or label
3574 src_name = active.name
3575 if active.label:
3576 src_name = active.label
3577 elif use_outputs_names:
3578 src_name = (out.name, )
3579 for rlo in rl_outputs:
3580 if out.name in {rlo.output_name, rlo.exr_output_name}:
3581 src_name = (rlo.output_name, rlo.exr_output_name)
3582 if dst_name not in src_name:
3583 valid = False
3584 if valid:
3585 for input in node.inputs:
3586 if input.type == out.type or node.type == 'REROUTE':
3587 if replace or not input.is_linked:
3588 links.new(out, input)
3589 if not use_node_name and not use_outputs_names:
3590 doit = False
3591 break
3593 return {'FINISHED'}
3596 class NWAlignNodes(Operator, NWBase):
3597 '''Align the selected nodes neatly in a row/column'''
3598 bl_idname = "node.nw_align_nodes"
3599 bl_label = "Align Nodes"
3600 bl_options = {'REGISTER', 'UNDO'}
3601 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3603 def execute(self, context):
3604 nodes, links = get_nodes_links(context)
3605 margin = self.margin
3607 selection = []
3608 for node in nodes:
3609 if node.select and node.type != 'FRAME':
3610 selection.append(node)
3612 # If no nodes are selected, align all nodes
3613 active_loc = None
3614 if not selection:
3615 selection = nodes
3616 elif nodes.active in selection:
3617 active_loc = copy(nodes.active.location) # make a copy, not a reference
3619 # Check if nodes should be laid out horizontally or vertically
3620 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3621 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3622 x_range = max(x_locs) - min(x_locs)
3623 y_range = max(y_locs) - min(y_locs)
3624 mid_x = (max(x_locs) + min(x_locs)) / 2
3625 mid_y = (max(y_locs) + min(y_locs)) / 2
3626 horizontal = x_range > y_range
3628 # Sort selection by location of node mid-point
3629 if horizontal:
3630 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3631 else:
3632 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3634 # Alignment
3635 current_pos = 0
3636 for node in selection:
3637 current_margin = margin
3638 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3640 if horizontal:
3641 node.location.x = current_pos
3642 current_pos += current_margin + node.dimensions.x
3643 node.location.y = mid_y + (node.dimensions.y / 2)
3644 else:
3645 node.location.y = current_pos
3646 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3647 node.location.x = mid_x - (node.dimensions.x / 2)
3649 # If active node is selected, center nodes around it
3650 if active_loc is not None:
3651 active_loc_diff = active_loc - nodes.active.location
3652 for node in selection:
3653 node.location += active_loc_diff
3654 else: # Position nodes centered around where they used to be
3655 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])
3656 new_mid = (max(locs) + min(locs)) / 2
3657 for node in selection:
3658 if horizontal:
3659 node.location.x += (mid_x - new_mid)
3660 else:
3661 node.location.y += (mid_y - new_mid)
3663 return {'FINISHED'}
3666 class NWSelectParentChildren(Operator, NWBase):
3667 bl_idname = "node.nw_select_parent_child"
3668 bl_label = "Select Parent or Children"
3669 bl_options = {'REGISTER', 'UNDO'}
3671 option: EnumProperty(
3672 name="option",
3673 items=(
3674 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3675 ('CHILD', 'Select Children', 'Select members of selected frame'),
3679 def execute(self, context):
3680 nodes, links = get_nodes_links(context)
3681 option = self.option
3682 selected = [node for node in nodes if node.select]
3683 if option == 'PARENT':
3684 for sel in selected:
3685 parent = sel.parent
3686 if parent:
3687 parent.select = True
3688 else: # option == 'CHILD'
3689 for sel in selected:
3690 children = [node for node in nodes if node.parent == sel]
3691 for kid in children:
3692 kid.select = True
3694 return {'FINISHED'}
3697 class NWDetachOutputs(Operator, NWBase):
3698 """Detach outputs of selected node leaving inputs linked"""
3699 bl_idname = "node.nw_detach_outputs"
3700 bl_label = "Detach Outputs"
3701 bl_options = {'REGISTER', 'UNDO'}
3703 def execute(self, context):
3704 nodes, links = get_nodes_links(context)
3705 selected = context.selected_nodes
3706 bpy.ops.node.duplicate_move_keep_inputs()
3707 new_nodes = context.selected_nodes
3708 bpy.ops.node.select_all(action="DESELECT")
3709 for node in selected:
3710 node.select = True
3711 bpy.ops.node.delete_reconnect()
3712 for new_node in new_nodes:
3713 new_node.select = True
3714 bpy.ops.transform.translate('INVOKE_DEFAULT')
3716 return {'FINISHED'}
3719 class NWLinkToOutputNode(Operator):
3720 """Link to Composite node or Material Output node"""
3721 bl_idname = "node.nw_link_out"
3722 bl_label = "Connect to Output"
3723 bl_options = {'REGISTER', 'UNDO'}
3725 @classmethod
3726 def poll(cls, context):
3727 valid = False
3728 if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
3729 if context.active_node is not None:
3730 for out in context.active_node.outputs:
3731 if is_visible_socket(out):
3732 valid = True
3733 break
3734 return valid
3736 def execute(self, context):
3737 nodes, links = get_nodes_links(context)
3738 active = nodes.active
3739 output_node = None
3740 output_index = None
3741 tree_type = context.space_data.tree_type
3742 output_types_shaders = [x[1] for x in shaders_output_nodes_props]
3743 output_types_compo = ['COMPOSITE']
3744 output_types_blender_mat = ['OUTPUT']
3745 output_types_textures = ['OUTPUT']
3746 output_types = output_types_shaders + output_types_compo + output_types_blender_mat
3747 for node in nodes:
3748 if node.type in output_types:
3749 output_node = node
3750 break
3751 if not output_node:
3752 bpy.ops.node.select_all(action="DESELECT")
3753 if tree_type == 'ShaderNodeTree':
3754 output_node = nodes.new('ShaderNodeOutputMaterial')
3755 elif tree_type == 'CompositorNodeTree':
3756 output_node = nodes.new('CompositorNodeComposite')
3757 elif tree_type == 'TextureNodeTree':
3758 output_node = nodes.new('TextureNodeOutput')
3759 output_node.location.x = active.location.x + active.dimensions.x + 80
3760 output_node.location.y = active.location.y
3761 if (output_node and active.outputs):
3762 for i, output in enumerate(active.outputs):
3763 if is_visible_socket(output):
3764 output_index = i
3765 break
3766 for i, output in enumerate(active.outputs):
3767 if output.type == output_node.inputs[0].type and is_visible_socket(output):
3768 output_index = i
3769 break
3771 out_input_index = 0
3772 if tree_type == 'ShaderNodeTree':
3773 if active.outputs[output_index].name == 'Volume':
3774 out_input_index = 1
3775 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3776 out_input_index = 2
3777 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3779 force_update(context) # viewport render does not update
3781 return {'FINISHED'}
3784 class NWMakeLink(Operator, NWBase):
3785 """Make a link from one socket to another"""
3786 bl_idname = 'node.nw_make_link'
3787 bl_label = 'Make Link'
3788 bl_options = {'REGISTER', 'UNDO'}
3789 from_socket: IntProperty()
3790 to_socket: IntProperty()
3792 def execute(self, context):
3793 nodes, links = get_nodes_links(context)
3795 n1 = nodes[context.scene.NWLazySource]
3796 n2 = nodes[context.scene.NWLazyTarget]
3798 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3800 force_update(context)
3802 return {'FINISHED'}
3805 class NWCallInputsMenu(Operator, NWBase):
3806 """Link from this output"""
3807 bl_idname = 'node.nw_call_inputs_menu'
3808 bl_label = 'Make Link'
3809 bl_options = {'REGISTER', 'UNDO'}
3810 from_socket: IntProperty()
3812 def execute(self, context):
3813 nodes, links = get_nodes_links(context)
3815 context.scene.NWSourceSocket = self.from_socket
3817 n1 = nodes[context.scene.NWLazySource]
3818 n2 = nodes[context.scene.NWLazyTarget]
3819 if len(n2.inputs) > 1:
3820 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3821 elif len(n2.inputs) == 1:
3822 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3823 return {'FINISHED'}
3826 class NWAddSequence(Operator, NWBase, ImportHelper):
3827 """Add an Image Sequence"""
3828 bl_idname = 'node.nw_add_sequence'
3829 bl_label = 'Import Image Sequence'
3830 bl_options = {'REGISTER', 'UNDO'}
3832 directory: StringProperty(
3833 subtype="DIR_PATH"
3835 filename: StringProperty(
3836 subtype="FILE_NAME"
3838 files: CollectionProperty(
3839 type=bpy.types.OperatorFileListElement,
3840 options={'HIDDEN', 'SKIP_SAVE'}
3843 def execute(self, context):
3844 nodes, links = get_nodes_links(context)
3845 directory = self.directory
3846 filename = self.filename
3847 files = self.files
3848 tree = context.space_data.node_tree
3850 # DEBUG
3851 # print ("\nDIR:", directory)
3852 # print ("FN:", filename)
3853 # print ("Fs:", list(f.name for f in files), '\n')
3855 if tree.type == 'SHADER':
3856 node_type = "ShaderNodeTexImage"
3857 elif tree.type == 'COMPOSITING':
3858 node_type = "CompositorNodeImage"
3859 else:
3860 self.report({'ERROR'}, "Unsupported Node Tree type!")
3861 return {'CANCELLED'}
3863 if not files[0].name and not filename:
3864 self.report({'ERROR'}, "No file chosen")
3865 return {'CANCELLED'}
3866 elif files[0].name and (not filename or not path.exists(directory+filename)):
3867 # User has selected multiple files without an active one, or the active one is non-existant
3868 filename = files[0].name
3870 if not path.exists(directory+filename):
3871 self.report({'ERROR'}, filename+" does not exist!")
3872 return {'CANCELLED'}
3874 without_ext = '.'.join(filename.split('.')[:-1])
3876 # if last digit isn't a number, it's not a sequence
3877 if not without_ext[-1].isdigit():
3878 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3879 return {'CANCELLED'}
3882 extension = filename.split('.')[-1]
3883 reverse = without_ext[::-1] # reverse string
3885 count_numbers = 0
3886 for char in reverse:
3887 if char.isdigit():
3888 count_numbers += 1
3889 else:
3890 break
3892 without_num = without_ext[:count_numbers*-1]
3894 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3896 num_frames = len(files)
3898 nodes_list = [node for node in nodes]
3899 if nodes_list:
3900 nodes_list.sort(key=lambda k: k.location.x)
3901 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3902 yloc = 0
3903 for node in nodes:
3904 node.select = False
3905 yloc += node_mid_pt(node, 'y')
3906 yloc = yloc/len(nodes)
3907 else:
3908 xloc = 0
3909 yloc = 0
3911 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3913 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3914 node = nodes.active
3915 node.label = name_with_hashes
3917 img = bpy.data.images.load(directory+(without_ext+'.'+extension))
3918 img.source = 'SEQUENCE'
3919 img.name = name_with_hashes
3920 node.image = img
3921 image_user = node.image_user if tree.type == 'SHADER' else node
3922 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
3923 image_user.frame_duration = num_frames
3925 return {'FINISHED'}
3928 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
3929 """Add multiple images at once"""
3930 bl_idname = 'node.nw_add_multiple_images'
3931 bl_label = 'Open Selected Images'
3932 bl_options = {'REGISTER', 'UNDO'}
3933 directory: StringProperty(
3934 subtype="DIR_PATH"
3936 files: CollectionProperty(
3937 type=bpy.types.OperatorFileListElement,
3938 options={'HIDDEN', 'SKIP_SAVE'}
3941 def execute(self, context):
3942 nodes, links = get_nodes_links(context)
3944 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3946 if context.space_data.node_tree.type == 'SHADER':
3947 node_type = "ShaderNodeTexImage"
3948 elif context.space_data.node_tree.type == 'COMPOSITING':
3949 node_type = "CompositorNodeImage"
3950 else:
3951 self.report({'ERROR'}, "Unsupported Node Tree type!")
3952 return {'CANCELLED'}
3954 new_nodes = []
3955 for f in self.files:
3956 fname = f.name
3958 node = nodes.new(node_type)
3959 new_nodes.append(node)
3960 node.label = fname
3961 node.hide = True
3962 node.width_hidden = 100
3963 node.location.x = xloc
3964 node.location.y = yloc
3965 yloc -= 40
3967 img = bpy.data.images.load(self.directory+fname)
3968 node.image = img
3970 # shift new nodes up to center of tree
3971 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3972 for node in nodes:
3973 if node in new_nodes:
3974 node.select = True
3975 node.location.y += (list_size/2)
3976 else:
3977 node.select = False
3978 return {'FINISHED'}
3981 class NWViewerFocus(bpy.types.Operator):
3982 """Set the viewer tile center to the mouse position"""
3983 bl_idname = "node.nw_viewer_focus"
3984 bl_label = "Viewer Focus"
3986 x: bpy.props.IntProperty()
3987 y: bpy.props.IntProperty()
3989 @classmethod
3990 def poll(cls, context):
3991 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3993 def execute(self, context):
3994 return {'FINISHED'}
3996 def invoke(self, context, event):
3997 render = context.scene.render
3998 space = context.space_data
3999 percent = render.resolution_percentage*0.01
4001 nodes, links = get_nodes_links(context)
4002 viewers = [n for n in nodes if n.type == 'VIEWER']
4004 if viewers:
4005 mlocx = event.mouse_region_x
4006 mlocy = event.mouse_region_y
4007 select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
4009 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
4010 region_x = context.region.width
4011 region_y = context.region.height
4013 region_center_x = context.region.width / 2
4014 region_center_y = context.region.height / 2
4016 bd_x = render.resolution_x * percent * space.backdrop_zoom
4017 bd_y = render.resolution_y * percent * space.backdrop_zoom
4019 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
4020 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
4022 margin_x = region_center_x - backdrop_center_x
4023 margin_y = region_center_y - backdrop_center_y
4025 abs_mouse_x = (mlocx - margin_x) / bd_x
4026 abs_mouse_y = (mlocy - margin_y) / bd_y
4028 for node in viewers:
4029 node.center_x = abs_mouse_x
4030 node.center_y = abs_mouse_y
4031 else:
4032 return {'PASS_THROUGH'}
4034 return self.execute(context)
4037 class NWSaveViewer(bpy.types.Operator, ExportHelper):
4038 """Save the current viewer node to an image file"""
4039 bl_idname = "node.nw_save_viewer"
4040 bl_label = "Save This Image"
4041 filepath: StringProperty(subtype="FILE_PATH")
4042 filename_ext: EnumProperty(
4043 name="Format",
4044 description="Choose the file format to save to",
4045 items=(('.bmp', "BMP", ""),
4046 ('.rgb', 'IRIS', ""),
4047 ('.png', 'PNG', ""),
4048 ('.jpg', 'JPEG', ""),
4049 ('.jp2', 'JPEG2000', ""),
4050 ('.tga', 'TARGA', ""),
4051 ('.cin', 'CINEON', ""),
4052 ('.dpx', 'DPX', ""),
4053 ('.exr', 'OPEN_EXR', ""),
4054 ('.hdr', 'HDR', ""),
4055 ('.tif', 'TIFF', "")),
4056 default='.png',
4059 @classmethod
4060 def poll(cls, context):
4061 valid = False
4062 if nw_check(context):
4063 if context.space_data.tree_type == 'CompositorNodeTree':
4064 if "Viewer Node" in [i.name for i in bpy.data.images]:
4065 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
4066 valid = True
4067 return valid
4069 def execute(self, context):
4070 fp = self.filepath
4071 if fp:
4072 formats = {
4073 '.bmp': 'BMP',
4074 '.rgb': 'IRIS',
4075 '.png': 'PNG',
4076 '.jpg': 'JPEG',
4077 '.jpeg': 'JPEG',
4078 '.jp2': 'JPEG2000',
4079 '.tga': 'TARGA',
4080 '.cin': 'CINEON',
4081 '.dpx': 'DPX',
4082 '.exr': 'OPEN_EXR',
4083 '.hdr': 'HDR',
4084 '.tiff': 'TIFF',
4085 '.tif': 'TIFF'}
4086 basename, ext = path.splitext(fp)
4087 old_render_format = context.scene.render.image_settings.file_format
4088 context.scene.render.image_settings.file_format = formats[self.filename_ext]
4089 context.area.type = "IMAGE_EDITOR"
4090 context.area.spaces[0].image = bpy.data.images['Viewer Node']
4091 context.area.spaces[0].image.save_render(fp)
4092 context.area.type = "NODE_EDITOR"
4093 context.scene.render.image_settings.file_format = old_render_format
4094 return {'FINISHED'}
4097 class NWResetNodes(bpy.types.Operator):
4098 """Reset Nodes in Selection"""
4099 bl_idname = "node.nw_reset_nodes"
4100 bl_label = "Reset Nodes"
4101 bl_options = {'REGISTER', 'UNDO'}
4103 @classmethod
4104 def poll(cls, context):
4105 space = context.space_data
4106 return space.type == 'NODE_EDITOR'
4108 def execute(self, context):
4109 node_active = context.active_node
4110 node_selected = context.selected_nodes
4111 node_ignore = ["FRAME","REROUTE", "GROUP"]
4113 # Check if one node is selected at least
4114 if not (len(node_selected) > 0):
4115 self.report({'ERROR'}, "1 node must be selected at least")
4116 return {'CANCELLED'}
4118 active_node_name = node_active.name if node_active.select else None
4119 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
4121 # Create output lists
4122 selected_node_names = [n.name for n in node_selected]
4123 success_names = []
4125 # Reset all valid children in a frame
4126 node_active_is_frame = False
4127 if len(node_selected) == 1 and node_active.type == "FRAME":
4128 node_tree = node_active.id_data
4129 children = [n for n in node_tree.nodes if n.parent == node_active]
4130 if children:
4131 valid_nodes = [n for n in children if n.type not in node_ignore]
4132 selected_node_names = [n.name for n in children if n.type not in node_ignore]
4133 node_active_is_frame = True
4135 # Check if valid nodes in selection
4136 if not (len(valid_nodes) > 0):
4137 # Check for frames only
4138 frames_selected = [n for n in node_selected if n.type == "FRAME"]
4139 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
4140 self.report({'ERROR'}, "Please select only 1 frame to reset")
4141 else:
4142 self.report({'ERROR'}, "No valid node(s) in selection")
4143 return {'CANCELLED'}
4145 # Report nodes that are not valid
4146 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
4147 valid_node_names = [n.name for n in valid_nodes]
4148 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
4149 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
4151 # Deselect all nodes
4152 for i in node_selected:
4153 i.select = False
4155 # Run through all valid nodes
4156 for node in valid_nodes:
4158 parent = node.parent if node.parent else None
4159 node_loc = [node.location.x, node.location.y]
4161 node_tree = node.id_data
4162 props_to_copy = 'bl_idname name location height width'.split(' ')
4164 reconnections = []
4165 mappings = chain.from_iterable([node.inputs, node.outputs])
4166 for i in (i for i in mappings if i.is_linked):
4167 for L in i.links:
4168 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
4170 props = {j: getattr(node, j) for j in props_to_copy}
4172 new_node = node_tree.nodes.new(props['bl_idname'])
4173 props_to_copy.pop(0)
4175 for prop in props_to_copy:
4176 setattr(new_node, prop, props[prop])
4178 nodes = node_tree.nodes
4179 nodes.remove(node)
4180 new_node.name = props['name']
4182 if parent:
4183 new_node.parent = parent
4184 new_node.location = node_loc
4186 for str_from, str_to in reconnections:
4187 node_tree.links.new(eval(str_from), eval(str_to))
4189 new_node.select = False
4190 success_names.append(new_node.name)
4192 # Reselect all nodes
4193 if selected_node_names and node_active_is_frame is False:
4194 for i in selected_node_names:
4195 node_tree.nodes[i].select = True
4197 if active_node_name is not None:
4198 node_tree.nodes[active_node_name].select = True
4199 node_tree.nodes.active = node_tree.nodes[active_node_name]
4201 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
4202 return {'FINISHED'}
4206 # P A N E L
4209 def drawlayout(context, layout, mode='non-panel'):
4210 tree_type = context.space_data.tree_type
4212 col = layout.column(align=True)
4213 col.menu(NWMergeNodesMenu.bl_idname)
4214 col.separator()
4216 col = layout.column(align=True)
4217 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
4218 col.separator()
4220 if tree_type == 'ShaderNodeTree':
4221 col = layout.column(align=True)
4222 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
4223 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
4224 col.separator()
4226 col = layout.column(align=True)
4227 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
4228 col.operator(NWSwapLinks.bl_idname)
4229 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
4230 col.separator()
4232 col = layout.column(align=True)
4233 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
4234 if tree_type != 'GeometryNodeTree':
4235 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
4236 col.separator()
4238 col = layout.column(align=True)
4239 if mode == 'panel':
4240 row = col.row(align=True)
4241 row.operator(NWClearLabel.bl_idname).option = True
4242 row.operator(NWModifyLabels.bl_idname)
4243 else:
4244 col.operator(NWClearLabel.bl_idname).option = True
4245 col.operator(NWModifyLabels.bl_idname)
4246 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
4247 col.separator()
4248 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
4249 col.separator()
4251 col = layout.column(align=True)
4252 if tree_type == 'CompositorNodeTree':
4253 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
4254 if tree_type != 'GeometryNodeTree':
4255 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
4256 col.separator()
4258 col = layout.column(align=True)
4259 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
4260 col.separator()
4262 col = layout.column(align=True)
4263 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
4264 col.separator()
4266 col = layout.column(align=True)
4267 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
4268 col.separator()
4271 class NodeWranglerPanel(Panel, NWBase):
4272 bl_idname = "NODE_PT_nw_node_wrangler"
4273 bl_space_type = 'NODE_EDITOR'
4274 bl_label = "Node Wrangler"
4275 bl_region_type = "UI"
4276 bl_category = "Node Wrangler"
4278 prepend: StringProperty(
4279 name='prepend',
4281 append: StringProperty()
4282 remove: StringProperty()
4284 def draw(self, context):
4285 self.layout.label(text="(Quick access: Shift+W)")
4286 drawlayout(context, self.layout, mode='panel')
4290 # M E N U S
4292 class NodeWranglerMenu(Menu, NWBase):
4293 bl_idname = "NODE_MT_nw_node_wrangler_menu"
4294 bl_label = "Node Wrangler"
4296 def draw(self, context):
4297 self.layout.operator_context = 'INVOKE_DEFAULT'
4298 drawlayout(context, self.layout)
4301 class NWMergeNodesMenu(Menu, NWBase):
4302 bl_idname = "NODE_MT_nw_merge_nodes_menu"
4303 bl_label = "Merge Selected Nodes"
4305 def draw(self, context):
4306 type = context.space_data.tree_type
4307 layout = self.layout
4308 if type == 'ShaderNodeTree':
4309 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
4310 if type == 'GeometryNodeTree':
4311 layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes")
4312 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
4313 else:
4314 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
4315 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
4316 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
4317 props.mode = 'MIX'
4318 props.merge_type = 'ZCOMBINE'
4319 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
4320 props.mode = 'MIX'
4321 props.merge_type = 'ALPHAOVER'
4323 class NWMergeGeometryMenu(Menu, NWBase):
4324 bl_idname = "NODE_MT_nw_merge_geometry_menu"
4325 bl_label = "Merge Selected Nodes using Geometry Nodes"
4326 def draw(self, context):
4327 layout = self.layout
4328 # The boolean node + Join Geometry node
4329 for type, name, description in geo_combine_operations:
4330 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4331 props.mode = type
4332 props.merge_type = 'GEOMETRY'
4334 class NWMergeShadersMenu(Menu, NWBase):
4335 bl_idname = "NODE_MT_nw_merge_shaders_menu"
4336 bl_label = "Merge Selected Nodes using Shaders"
4338 def draw(self, context):
4339 layout = self.layout
4340 for type in ('MIX', 'ADD'):
4341 props = layout.operator(NWMergeNodes.bl_idname, text=type)
4342 props.mode = type
4343 props.merge_type = 'SHADER'
4346 class NWMergeMixMenu(Menu, NWBase):
4347 bl_idname = "NODE_MT_nw_merge_mix_menu"
4348 bl_label = "Merge Selected Nodes using Mix"
4350 def draw(self, context):
4351 layout = self.layout
4352 for type, name, description in blend_types:
4353 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4354 props.mode = type
4355 props.merge_type = 'MIX'
4358 class NWConnectionListOutputs(Menu, NWBase):
4359 bl_idname = "NODE_MT_nw_connection_list_out"
4360 bl_label = "From:"
4362 def draw(self, context):
4363 layout = self.layout
4364 nodes, links = get_nodes_links(context)
4366 n1 = nodes[context.scene.NWLazySource]
4367 index=0
4368 for o in n1.outputs:
4369 # Only show sockets that are exposed.
4370 if o.enabled:
4371 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
4372 index+=1
4375 class NWConnectionListInputs(Menu, NWBase):
4376 bl_idname = "NODE_MT_nw_connection_list_in"
4377 bl_label = "To:"
4379 def draw(self, context):
4380 layout = self.layout
4381 nodes, links = get_nodes_links(context)
4383 n2 = nodes[context.scene.NWLazyTarget]
4385 index = 0
4386 for i in n2.inputs:
4387 # Only show sockets that are exposed.
4388 # This prevents, for example, the scale value socket
4389 # of the vector math node being added to the list when
4390 # the mode is not 'SCALE'.
4391 if i.enabled:
4392 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
4393 op.from_socket = context.scene.NWSourceSocket
4394 op.to_socket = index
4395 index+=1
4398 class NWMergeMathMenu(Menu, NWBase):
4399 bl_idname = "NODE_MT_nw_merge_math_menu"
4400 bl_label = "Merge Selected Nodes using Math"
4402 def draw(self, context):
4403 layout = self.layout
4404 for type, name, description in operations:
4405 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4406 props.mode = type
4407 props.merge_type = 'MATH'
4410 class NWBatchChangeNodesMenu(Menu, NWBase):
4411 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
4412 bl_label = "Batch Change Selected Nodes"
4414 def draw(self, context):
4415 layout = self.layout
4416 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
4417 layout.menu(NWBatchChangeOperationMenu.bl_idname)
4420 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
4421 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
4422 bl_label = "Batch Change Blend Type"
4424 def draw(self, context):
4425 layout = self.layout
4426 for type, name, description in blend_types:
4427 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
4428 props.blend_type = type
4429 props.operation = 'CURRENT'
4432 class NWBatchChangeOperationMenu(Menu, NWBase):
4433 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
4434 bl_label = "Batch Change Math Operation"
4436 def draw(self, context):
4437 layout = self.layout
4438 for type, name, description in operations:
4439 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
4440 props.blend_type = 'CURRENT'
4441 props.operation = type
4444 class NWCopyToSelectedMenu(Menu, NWBase):
4445 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
4446 bl_label = "Copy to Selected"
4448 def draw(self, context):
4449 layout = self.layout
4450 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
4451 layout.menu(NWCopyLabelMenu.bl_idname)
4454 class NWCopyLabelMenu(Menu, NWBase):
4455 bl_idname = "NODE_MT_nw_copy_label_menu"
4456 bl_label = "Copy Label"
4458 def draw(self, context):
4459 layout = self.layout
4460 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
4461 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
4462 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
4465 class NWAddReroutesMenu(Menu, NWBase):
4466 bl_idname = "NODE_MT_nw_add_reroutes_menu"
4467 bl_label = "Add Reroutes"
4468 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
4470 def draw(self, context):
4471 layout = self.layout
4472 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
4473 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
4474 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
4477 class NWLinkActiveToSelectedMenu(Menu, NWBase):
4478 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
4479 bl_label = "Link Active to Selected"
4481 def draw(self, context):
4482 layout = self.layout
4483 layout.menu(NWLinkStandardMenu.bl_idname)
4484 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
4485 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
4488 class NWLinkStandardMenu(Menu, NWBase):
4489 bl_idname = "NODE_MT_nw_link_standard_menu"
4490 bl_label = "To All Selected"
4492 def draw(self, context):
4493 layout = self.layout
4494 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4495 props.replace = False
4496 props.use_node_name = False
4497 props.use_outputs_names = False
4498 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4499 props.replace = True
4500 props.use_node_name = False
4501 props.use_outputs_names = False
4504 class NWLinkUseNodeNameMenu(Menu, NWBase):
4505 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
4506 bl_label = "Use Node Name/Label"
4508 def draw(self, context):
4509 layout = self.layout
4510 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4511 props.replace = False
4512 props.use_node_name = True
4513 props.use_outputs_names = False
4514 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4515 props.replace = True
4516 props.use_node_name = True
4517 props.use_outputs_names = False
4520 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
4521 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
4522 bl_label = "Use Outputs Names"
4524 def draw(self, context):
4525 layout = self.layout
4526 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4527 props.replace = False
4528 props.use_node_name = False
4529 props.use_outputs_names = True
4530 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4531 props.replace = True
4532 props.use_node_name = False
4533 props.use_outputs_names = True
4536 class NWVertColMenu(bpy.types.Menu):
4537 bl_idname = "NODE_MT_nw_node_vertex_color_menu"
4538 bl_label = "Vertex Colors"
4540 @classmethod
4541 def poll(cls, context):
4542 valid = False
4543 if nw_check(context):
4544 snode = context.space_data
4545 valid = snode.tree_type == 'ShaderNodeTree'
4546 return valid
4548 def draw(self, context):
4549 l = self.layout
4550 nodes, links = get_nodes_links(context)
4551 mat = context.object.active_material
4553 objs = []
4554 for obj in bpy.data.objects:
4555 for slot in obj.material_slots:
4556 if slot.material == mat:
4557 objs.append(obj)
4558 vcols = []
4559 for obj in objs:
4560 if obj.data.vertex_colors:
4561 for vcol in obj.data.vertex_colors:
4562 vcols.append(vcol.name)
4563 vcols = list(set(vcols)) # get a unique list
4565 if vcols:
4566 for vcol in vcols:
4567 l.operator(NWAddAttrNode.bl_idname, text=vcol).attr_name = vcol
4568 else:
4569 l.label(text="No Vertex Color layers on objects with this material")
4572 class NWSwitchNodeTypeMenu(Menu, NWBase):
4573 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4574 bl_label = "Switch Type to..."
4576 def draw(self, context):
4577 layout = self.layout
4578 tree = context.space_data.node_tree
4579 if tree.type == 'SHADER':
4580 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
4581 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
4582 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
4583 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
4584 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
4585 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
4586 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
4587 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
4588 if tree.type == 'COMPOSITING':
4589 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
4590 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
4591 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
4592 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
4593 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
4594 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
4595 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
4596 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
4597 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
4598 if tree.type == 'TEXTURE':
4599 layout.menu(NWSwitchTexInputSubmenu.bl_idname)
4600 layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
4601 layout.menu(NWSwitchTexColorSubmenu.bl_idname)
4602 layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
4603 layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
4604 layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
4605 layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
4606 layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
4607 if tree.type == 'GEOMETRY':
4608 categories = [c for c in node_categories_iter(context)
4609 if c.name not in ['Group', 'Script']]
4610 for cat in categories:
4611 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4612 if hasattr(bpy.types, idname):
4613 layout.menu(idname)
4614 else:
4615 layout.label(text="Unable to load altered node lists.")
4616 layout.label(text="Please re-enable Node Wrangler.")
4617 break
4620 class NWSwitchShadersInputSubmenu(Menu, NWBase):
4621 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
4622 bl_label = "Input"
4624 def draw(self, context):
4625 layout = self.layout
4626 for ident, node_type, rna_name in shaders_input_nodes_props:
4627 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4628 props.to_type = ident
4631 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
4632 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
4633 bl_label = "Output"
4635 def draw(self, context):
4636 layout = self.layout
4637 for ident, node_type, rna_name in shaders_output_nodes_props:
4638 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4639 props.to_type = ident
4642 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
4643 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
4644 bl_label = "Shader"
4646 def draw(self, context):
4647 layout = self.layout
4648 for ident, node_type, rna_name in shaders_shader_nodes_props:
4649 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4650 props.to_type = ident
4653 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
4654 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
4655 bl_label = "Texture"
4657 def draw(self, context):
4658 layout = self.layout
4659 for ident, node_type, rna_name in shaders_texture_nodes_props:
4660 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4661 props.to_type = ident
4664 class NWSwitchShadersColorSubmenu(Menu, NWBase):
4665 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
4666 bl_label = "Color"
4668 def draw(self, context):
4669 layout = self.layout
4670 for ident, node_type, rna_name in shaders_color_nodes_props:
4671 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4672 props.to_type = ident
4675 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
4676 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
4677 bl_label = "Vector"
4679 def draw(self, context):
4680 layout = self.layout
4681 for ident, node_type, rna_name in shaders_vector_nodes_props:
4682 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4683 props.to_type = ident
4686 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
4687 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
4688 bl_label = "Converter"
4690 def draw(self, context):
4691 layout = self.layout
4692 for ident, node_type, rna_name in shaders_converter_nodes_props:
4693 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4694 props.to_type = ident
4697 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
4698 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
4699 bl_label = "Layout"
4701 def draw(self, context):
4702 layout = self.layout
4703 for ident, node_type, rna_name in shaders_layout_nodes_props:
4704 if node_type != 'FRAME':
4705 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4706 props.to_type = ident
4709 class NWSwitchCompoInputSubmenu(Menu, NWBase):
4710 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
4711 bl_label = "Input"
4713 def draw(self, context):
4714 layout = self.layout
4715 for ident, node_type, rna_name in compo_input_nodes_props:
4716 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4717 props.to_type = ident
4720 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
4721 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
4722 bl_label = "Output"
4724 def draw(self, context):
4725 layout = self.layout
4726 for ident, node_type, rna_name in compo_output_nodes_props:
4727 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4728 props.to_type = ident
4731 class NWSwitchCompoColorSubmenu(Menu, NWBase):
4732 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
4733 bl_label = "Color"
4735 def draw(self, context):
4736 layout = self.layout
4737 for ident, node_type, rna_name in compo_color_nodes_props:
4738 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4739 props.to_type = ident
4742 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
4743 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
4744 bl_label = "Converter"
4746 def draw(self, context):
4747 layout = self.layout
4748 for ident, node_type, rna_name in compo_converter_nodes_props:
4749 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4750 props.to_type = ident
4753 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
4754 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
4755 bl_label = "Filter"
4757 def draw(self, context):
4758 layout = self.layout
4759 for ident, node_type, rna_name in compo_filter_nodes_props:
4760 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4761 props.to_type = ident
4764 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
4765 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
4766 bl_label = "Vector"
4768 def draw(self, context):
4769 layout = self.layout
4770 for ident, node_type, rna_name in compo_vector_nodes_props:
4771 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4772 props.to_type = ident
4775 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
4776 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
4777 bl_label = "Matte"
4779 def draw(self, context):
4780 layout = self.layout
4781 for ident, node_type, rna_name in compo_matte_nodes_props:
4782 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4783 props.to_type = ident
4786 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
4787 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
4788 bl_label = "Distort"
4790 def draw(self, context):
4791 layout = self.layout
4792 for ident, node_type, rna_name in compo_distort_nodes_props:
4793 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4794 props.to_type = ident
4797 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
4798 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
4799 bl_label = "Layout"
4801 def draw(self, context):
4802 layout = self.layout
4803 for ident, node_type, rna_name in compo_layout_nodes_props:
4804 if node_type != 'FRAME':
4805 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4806 props.to_type = ident
4809 class NWSwitchMatInputSubmenu(Menu, NWBase):
4810 bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
4811 bl_label = "Input"
4813 def draw(self, context):
4814 layout = self.layout
4815 for ident, node_type, rna_name in sorted(blender_mat_input_nodes_props, key=lambda k: k[2]):
4816 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4817 props.to_type = ident
4820 class NWSwitchMatOutputSubmenu(Menu, NWBase):
4821 bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
4822 bl_label = "Output"
4824 def draw(self, context):
4825 layout = self.layout
4826 for ident, node_type, rna_name in sorted(blender_mat_output_nodes_props, key=lambda k: k[2]):
4827 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4828 props.to_type = ident
4831 class NWSwitchMatColorSubmenu(Menu, NWBase):
4832 bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
4833 bl_label = "Color"
4835 def draw(self, context):
4836 layout = self.layout
4837 for ident, node_type, rna_name in sorted(blender_mat_color_nodes_props, key=lambda k: k[2]):
4838 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4839 props.to_type = ident
4842 class NWSwitchMatVectorSubmenu(Menu, NWBase):
4843 bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
4844 bl_label = "Vector"
4846 def draw(self, context):
4847 layout = self.layout
4848 for ident, node_type, rna_name in sorted(blender_mat_vector_nodes_props, key=lambda k: k[2]):
4849 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4850 props.to_type = ident
4853 class NWSwitchMatConverterSubmenu(Menu, NWBase):
4854 bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
4855 bl_label = "Converter"
4857 def draw(self, context):
4858 layout = self.layout
4859 for ident, node_type, rna_name in sorted(blender_mat_converter_nodes_props, key=lambda k: k[2]):
4860 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4861 props.to_type = ident
4864 class NWSwitchMatLayoutSubmenu(Menu, NWBase):
4865 bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
4866 bl_label = "Layout"
4868 def draw(self, context):
4869 layout = self.layout
4870 for ident, node_type, rna_name in sorted(blender_mat_layout_nodes_props, key=lambda k: k[2]):
4871 if node_type != 'FRAME':
4872 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4873 props.to_type = ident
4876 class NWSwitchTexInputSubmenu(Menu, NWBase):
4877 bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
4878 bl_label = "Input"
4880 def draw(self, context):
4881 layout = self.layout
4882 for ident, node_type, rna_name in sorted(texture_input_nodes_props, key=lambda k: k[2]):
4883 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4884 props.to_type = ident
4887 class NWSwitchTexOutputSubmenu(Menu, NWBase):
4888 bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
4889 bl_label = "Output"
4891 def draw(self, context):
4892 layout = self.layout
4893 for ident, node_type, rna_name in sorted(texture_output_nodes_props, key=lambda k: k[2]):
4894 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4895 props.to_type = ident
4898 class NWSwitchTexColorSubmenu(Menu, NWBase):
4899 bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
4900 bl_label = "Color"
4902 def draw(self, context):
4903 layout = self.layout
4904 for ident, node_type, rna_name in sorted(texture_color_nodes_props, key=lambda k: k[2]):
4905 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4906 props.to_type = ident
4909 class NWSwitchTexPatternSubmenu(Menu, NWBase):
4910 bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
4911 bl_label = "Pattern"
4913 def draw(self, context):
4914 layout = self.layout
4915 for ident, node_type, rna_name in sorted(texture_pattern_nodes_props, key=lambda k: k[2]):
4916 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4917 props.to_type = ident
4920 class NWSwitchTexTexturesSubmenu(Menu, NWBase):
4921 bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
4922 bl_label = "Textures"
4924 def draw(self, context):
4925 layout = self.layout
4926 for ident, node_type, rna_name in sorted(texture_textures_nodes_props, key=lambda k: k[2]):
4927 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4928 props.to_type = ident
4931 class NWSwitchTexConverterSubmenu(Menu, NWBase):
4932 bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
4933 bl_label = "Converter"
4935 def draw(self, context):
4936 layout = self.layout
4937 for ident, node_type, rna_name in sorted(texture_converter_nodes_props, key=lambda k: k[2]):
4938 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4939 props.to_type = ident
4942 class NWSwitchTexDistortSubmenu(Menu, NWBase):
4943 bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
4944 bl_label = "Distort"
4946 def draw(self, context):
4947 layout = self.layout
4948 for ident, node_type, rna_name in sorted(texture_distort_nodes_props, key=lambda k: k[2]):
4949 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4950 props.to_type = ident
4953 class NWSwitchTexLayoutSubmenu(Menu, NWBase):
4954 bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
4955 bl_label = "Layout"
4957 def draw(self, context):
4958 layout = self.layout
4959 for ident, node_type, rna_name in sorted(texture_layout_nodes_props, key=lambda k: k[2]):
4960 if node_type != 'FRAME':
4961 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4962 props.to_type = ident
4964 def draw_switch_category_submenu(self, context):
4965 layout = self.layout
4966 if self.category.name == 'Layout':
4967 for node in self.category.items(context):
4968 if node.nodetype != 'NodeFrame':
4969 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4970 props.to_type = node.nodetype
4971 else:
4972 for node in self.category.items(context):
4973 if isinstance(node, NodeItemCustom):
4974 node.draw(self, layout, context)
4975 continue
4976 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4977 props.geo_to_type = node.nodetype
4980 # APPENDAGES TO EXISTING UI
4984 def select_parent_children_buttons(self, context):
4985 layout = self.layout
4986 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4987 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4990 def attr_nodes_menu_func(self, context):
4991 col = self.layout.column(align=True)
4992 col.menu("NODE_MT_nw_node_vertex_color_menu")
4993 col.separator()
4996 def multipleimages_menu_func(self, context):
4997 col = self.layout.column(align=True)
4998 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4999 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
5000 col.separator()
5003 def bgreset_menu_func(self, context):
5004 self.layout.operator(NWResetBG.bl_idname)
5007 def save_viewer_menu_func(self, context):
5008 if nw_check(context):
5009 if context.space_data.tree_type == 'CompositorNodeTree':
5010 if context.scene.node_tree.nodes.active:
5011 if context.scene.node_tree.nodes.active.type == "VIEWER":
5012 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
5015 def reset_nodes_button(self, context):
5016 node_active = context.active_node
5017 node_selected = context.selected_nodes
5018 node_ignore = ["FRAME","REROUTE", "GROUP"]
5020 # Check if active node is in the selection and respective type
5021 if (len(node_selected) == 1) and node_active.select and node_active.type not in node_ignore:
5022 row = self.layout.row()
5023 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
5024 self.layout.separator()
5026 elif (len(node_selected) == 1) and node_active.select and node_active.type == "FRAME":
5027 row = self.layout.row()
5028 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
5029 self.layout.separator()
5033 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5035 switch_category_menus = []
5036 addon_keymaps = []
5037 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5038 # props entry: (property name, property value)
5039 kmi_defs = (
5040 # MERGE NODES
5041 # NWMergeNodes with Ctrl (AUTO).
5042 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
5043 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5044 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
5045 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5046 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5047 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5048 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
5049 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5050 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5051 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5052 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
5053 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5054 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5055 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5056 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
5057 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5058 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5059 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5060 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
5061 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5062 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
5063 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5064 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
5065 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5066 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5067 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5068 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5069 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
5070 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5071 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
5072 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5073 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5074 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5075 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
5076 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5077 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5078 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5079 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
5080 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5081 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5082 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5083 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
5084 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5085 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5086 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5087 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
5088 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5089 # NWMergeNodes with Ctrl Shift (MATH)
5090 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5091 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5092 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
5093 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5094 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5095 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5096 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
5097 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5098 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5099 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5100 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
5101 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5102 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5103 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5104 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
5105 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5106 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
5107 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5108 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
5109 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5110 # BATCH CHANGE NODES
5111 # NWBatchChangeNodes with Alt
5112 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
5113 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5114 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
5115 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5116 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5117 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5118 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
5119 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5120 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5121 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5122 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
5123 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5124 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5125 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5126 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
5127 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5128 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5129 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5130 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
5131 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5132 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
5133 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5134 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
5135 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5136 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
5137 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5138 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
5139 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5140 # LINK ACTIVE TO SELECTED
5141 # Don't use names, don't replace links (K)
5142 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
5143 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5144 # Don't use names, replace links (Shift K)
5145 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
5146 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5147 # Use node name, don't replace links (')
5148 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
5149 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5150 # Use node name, replace links (Shift ')
5151 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
5152 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5153 # Don't use names, don't replace links (;)
5154 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
5155 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5156 # Don't use names, replace links (')
5157 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
5158 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5159 # CHANGE MIX FACTOR
5160 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5161 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5162 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5163 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5164 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5165 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5166 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5167 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5168 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5169 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5170 # CLEAR LABEL (Alt L)
5171 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5172 # MODIFY LABEL (Alt Shift L)
5173 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5174 # Copy Label from active to selected
5175 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5176 # DETACH OUTPUTS (Alt Shift D)
5177 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5178 # LINK TO OUTPUT NODE (O)
5179 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5180 # SELECT PARENT/CHILDREN
5181 # Select Children
5182 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5183 # Select Parent
5184 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5185 # Add Texture Setup
5186 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5187 # Add Principled BSDF Texture Setup
5188 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5189 # Reset backdrop
5190 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5191 # Delete unused
5192 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5193 # Frame Selected
5194 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5195 # Swap Outputs
5196 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5197 # Preview Node
5198 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5199 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5200 # Reload Images
5201 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
5202 # Lazy Mix
5203 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5204 # Lazy Connect
5205 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5206 # Lazy Connect with Menu
5207 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5208 # Viewer Tile Center
5209 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5210 # Align Nodes
5211 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5212 # Reset Nodes (Back Space)
5213 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5214 # MENUS
5215 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
5216 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
5217 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
5218 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
5219 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
5220 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
5224 classes = (
5225 NWPrincipledPreferences,
5226 NWNodeWrangler,
5227 NWLazyMix,
5228 NWLazyConnect,
5229 NWDeleteUnused,
5230 NWSwapLinks,
5231 NWResetBG,
5232 NWAddAttrNode,
5233 NWPreviewNode,
5234 NWFrameSelected,
5235 NWReloadImages,
5236 NWSwitchNodeType,
5237 NWMergeNodes,
5238 NWBatchChangeNodes,
5239 NWChangeMixFactor,
5240 NWCopySettings,
5241 NWCopyLabel,
5242 NWClearLabel,
5243 NWModifyLabels,
5244 NWAddTextureSetup,
5245 NWAddPrincipledSetup,
5246 NWAddReroutes,
5247 NWLinkActiveToSelected,
5248 NWAlignNodes,
5249 NWSelectParentChildren,
5250 NWDetachOutputs,
5251 NWLinkToOutputNode,
5252 NWMakeLink,
5253 NWCallInputsMenu,
5254 NWAddSequence,
5255 NWAddMultipleImages,
5256 NWViewerFocus,
5257 NWSaveViewer,
5258 NWResetNodes,
5259 NodeWranglerPanel,
5260 NodeWranglerMenu,
5261 NWMergeNodesMenu,
5262 NWMergeShadersMenu,
5263 NWMergeGeometryMenu,
5264 NWMergeMixMenu,
5265 NWConnectionListOutputs,
5266 NWConnectionListInputs,
5267 NWMergeMathMenu,
5268 NWBatchChangeNodesMenu,
5269 NWBatchChangeBlendTypeMenu,
5270 NWBatchChangeOperationMenu,
5271 NWCopyToSelectedMenu,
5272 NWCopyLabelMenu,
5273 NWAddReroutesMenu,
5274 NWLinkActiveToSelectedMenu,
5275 NWLinkStandardMenu,
5276 NWLinkUseNodeNameMenu,
5277 NWLinkUseOutputsNamesMenu,
5278 NWVertColMenu,
5279 NWSwitchNodeTypeMenu,
5280 NWSwitchShadersInputSubmenu,
5281 NWSwitchShadersOutputSubmenu,
5282 NWSwitchShadersShaderSubmenu,
5283 NWSwitchShadersTextureSubmenu,
5284 NWSwitchShadersColorSubmenu,
5285 NWSwitchShadersVectorSubmenu,
5286 NWSwitchShadersConverterSubmenu,
5287 NWSwitchShadersLayoutSubmenu,
5288 NWSwitchCompoInputSubmenu,
5289 NWSwitchCompoOutputSubmenu,
5290 NWSwitchCompoColorSubmenu,
5291 NWSwitchCompoConverterSubmenu,
5292 NWSwitchCompoFilterSubmenu,
5293 NWSwitchCompoVectorSubmenu,
5294 NWSwitchCompoMatteSubmenu,
5295 NWSwitchCompoDistortSubmenu,
5296 NWSwitchCompoLayoutSubmenu,
5297 NWSwitchMatInputSubmenu,
5298 NWSwitchMatOutputSubmenu,
5299 NWSwitchMatColorSubmenu,
5300 NWSwitchMatVectorSubmenu,
5301 NWSwitchMatConverterSubmenu,
5302 NWSwitchMatLayoutSubmenu,
5303 NWSwitchTexInputSubmenu,
5304 NWSwitchTexOutputSubmenu,
5305 NWSwitchTexColorSubmenu,
5306 NWSwitchTexPatternSubmenu,
5307 NWSwitchTexTexturesSubmenu,
5308 NWSwitchTexConverterSubmenu,
5309 NWSwitchTexDistortSubmenu,
5310 NWSwitchTexLayoutSubmenu,
5313 def register():
5314 from bpy.utils import register_class
5316 # props
5317 bpy.types.Scene.NWBusyDrawing = StringProperty(
5318 name="Busy Drawing!",
5319 default="",
5320 description="An internal property used to store only the first mouse position")
5321 bpy.types.Scene.NWLazySource = StringProperty(
5322 name="Lazy Source!",
5323 default="x",
5324 description="An internal property used to store the first node in a Lazy Connect operation")
5325 bpy.types.Scene.NWLazyTarget = StringProperty(
5326 name="Lazy Target!",
5327 default="x",
5328 description="An internal property used to store the last node in a Lazy Connect operation")
5329 bpy.types.Scene.NWSourceSocket = IntProperty(
5330 name="Source Socket!",
5331 default=0,
5332 description="An internal property used to store the source socket in a Lazy Connect operation")
5333 bpy.types.NodeSocketInterface.NWViewerSocket = BoolProperty(
5334 name="NW Socket",
5335 default=False,
5336 description="An internal property used to determine if a socket is generated by the addon"
5339 for cls in classes:
5340 register_class(cls)
5342 # keymaps
5343 addon_keymaps.clear()
5344 kc = bpy.context.window_manager.keyconfigs.addon
5345 if kc:
5346 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
5347 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
5348 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
5349 if props:
5350 for prop, value in props:
5351 setattr(kmi.properties, prop, value)
5352 addon_keymaps.append((km, kmi))
5354 # menu items
5355 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
5356 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
5357 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
5358 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
5359 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
5360 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
5361 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
5362 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
5364 # switch submenus
5365 switch_category_menus.clear()
5366 for cat in node_categories_iter(None):
5367 if cat.name not in ['Group', 'Script'] and cat.identifier.startswith('GEO'):
5368 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
5369 switch_category_type = type(idname, (bpy.types.Menu,), {
5370 "bl_space_type": 'NODE_EDITOR',
5371 "bl_label": cat.name,
5372 "category": cat,
5373 "poll": cat.poll,
5374 "draw": draw_switch_category_submenu,
5377 switch_category_menus.append(switch_category_type)
5379 bpy.utils.register_class(switch_category_type)
5382 def unregister():
5383 from bpy.utils import unregister_class
5385 # props
5386 del bpy.types.Scene.NWBusyDrawing
5387 del bpy.types.Scene.NWLazySource
5388 del bpy.types.Scene.NWLazyTarget
5389 del bpy.types.Scene.NWSourceSocket
5390 del bpy.types.NodeSocketInterface.NWViewerSocket
5392 for cat_types in switch_category_menus:
5393 bpy.utils.unregister_class(cat_types)
5394 switch_category_menus.clear()
5396 # keymaps
5397 for km, kmi in addon_keymaps:
5398 km.keymap_items.remove(kmi)
5399 addon_keymaps.clear()
5401 # menuitems
5402 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
5403 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
5404 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
5405 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
5406 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
5407 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
5408 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
5409 bpy.types.NODE_MT_node.remove(reset_nodes_button)
5411 for cls in classes:
5412 unregister_class(cls)
5414 if __name__ == "__main__":
5415 register()