AnimAll: Rename UVMap and Vertex Group settings again
[blender-addons.git] / node_wrangler.py
blob1a815dc63218c119f88c50719b4558a446c60561
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, 40),
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 Value'),
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 Value'),
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 Value'),
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
719 def abs_node_location(node):
720 abs_location = node.location
721 if node.parent is None:
722 return abs_location
723 return abs_location + abs_node_location(node.parent)
725 def node_at_pos(nodes, context, event):
726 nodes_under_mouse = []
727 target_node = None
729 store_mouse_cursor(context, event)
730 x, y = context.space_data.cursor_location
732 # Make a list of each corner (and middle of border) for each node.
733 # Will be sorted to find nearest point and thus nearest node
734 node_points_with_dist = []
735 for node in nodes:
736 skipnode = False
737 if node.type != 'FRAME': # no point trying to link to a frame node
738 dimx = node.dimensions.x/dpifac()
739 dimy = node.dimensions.y/dpifac()
740 locx, locy = abs_node_location(node)
742 if not skipnode:
743 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
744 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
745 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
746 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
748 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
749 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
750 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
751 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
753 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
755 for node in nodes:
756 if node.type != 'FRAME' and skipnode == False:
757 locx, locy = abs_node_location(node)
758 dimx = node.dimensions.x/dpifac()
759 dimy = node.dimensions.y/dpifac()
760 if (locx <= x <= locx + dimx) and \
761 (locy - dimy <= y <= locy):
762 nodes_under_mouse.append(node)
764 if len(nodes_under_mouse) == 1:
765 if nodes_under_mouse[0] != nearest_node:
766 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
767 else:
768 target_node = nearest_node # else use the nearest node
769 else:
770 target_node = nearest_node
771 return target_node
774 def store_mouse_cursor(context, event):
775 space = context.space_data
776 v2d = context.region.view2d
777 tree = space.edit_tree
779 # convert mouse position to the View2D for later node placement
780 if context.region.type == 'WINDOW':
781 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
782 else:
783 space.cursor_location = tree.view_center
785 def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
786 shader = gpu.shader.from_builtin('2D_SMOOTH_COLOR')
788 vertices = ((x1, y1), (x2, y2))
789 vertex_colors = ((colour[0]+(1.0-colour[0])/4,
790 colour[1]+(1.0-colour[1])/4,
791 colour[2]+(1.0-colour[2])/4,
792 colour[3]+(1.0-colour[3])/4),
793 colour)
795 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
796 bgl.glLineWidth(size * dpifac())
798 shader.bind()
799 batch.draw(shader)
802 def draw_circle_2d_filled(shader, mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
803 radius = radius * dpifac()
804 sides = 12
805 vertices = [(radius * cos(i * 2 * pi / sides) + mx,
806 radius * sin(i * 2 * pi / sides) + my)
807 for i in range(sides + 1)]
809 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
810 shader.bind()
811 shader.uniform_float("color", colour)
812 batch.draw(shader)
815 def draw_rounded_node_border(shader, node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
816 area_width = bpy.context.area.width - (16*dpifac()) - 1
817 bottom_bar = (16*dpifac()) + 1
818 sides = 16
819 radius = radius*dpifac()
821 nlocx, nlocy = abs_node_location(node)
823 nlocx = (nlocx+1)*dpifac()
824 nlocy = (nlocy+1)*dpifac()
825 ndimx = node.dimensions.x
826 ndimy = node.dimensions.y
828 if node.hide:
829 nlocx += -1
830 nlocy += 5
831 if node.type == 'REROUTE':
832 #nlocx += 1
833 nlocy -= 1
834 ndimx = 0
835 ndimy = 0
836 radius += 6
838 # Top left corner
839 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
840 vertices = [(mx,my)]
841 for i in range(sides+1):
842 if (4<=i<=8):
843 if my > bottom_bar and mx < area_width:
844 cosine = radius * cos(i * 2 * pi / sides) + mx
845 sine = radius * sin(i * 2 * pi / sides) + my
846 vertices.append((cosine,sine))
847 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
848 shader.bind()
849 shader.uniform_float("color", colour)
850 batch.draw(shader)
852 # Top right corner
853 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
854 vertices = [(mx,my)]
855 for i in range(sides+1):
856 if (0<=i<=4):
857 if my > bottom_bar and mx < area_width:
858 cosine = radius * cos(i * 2 * pi / sides) + mx
859 sine = radius * sin(i * 2 * pi / sides) + my
860 vertices.append((cosine,sine))
861 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
862 shader.bind()
863 shader.uniform_float("color", colour)
864 batch.draw(shader)
866 # Bottom left corner
867 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
868 vertices = [(mx,my)]
869 for i in range(sides+1):
870 if (8<=i<=12):
871 if my > bottom_bar and mx < area_width:
872 cosine = radius * cos(i * 2 * pi / sides) + mx
873 sine = radius * sin(i * 2 * pi / sides) + my
874 vertices.append((cosine,sine))
875 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
876 shader.bind()
877 shader.uniform_float("color", colour)
878 batch.draw(shader)
880 # Bottom right corner
881 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
882 vertices = [(mx,my)]
883 for i in range(sides+1):
884 if (12<=i<=16):
885 if my > bottom_bar and mx < area_width:
886 cosine = radius * cos(i * 2 * pi / sides) + mx
887 sine = radius * sin(i * 2 * pi / sides) + my
888 vertices.append((cosine,sine))
889 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
890 shader.bind()
891 shader.uniform_float("color", colour)
892 batch.draw(shader)
894 # prepare drawing all edges in one batch
895 vertices = []
896 indices = []
897 id_last = 0
899 # Left edge
900 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
901 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
902 if m1x < area_width and m2x < area_width:
903 vertices.extend([(m2x-radius,m2y), (m2x,m2y),
904 (m1x,m1y), (m1x-radius,m1y)])
905 indices.extend([(id_last, id_last+1, id_last+3),
906 (id_last+3, id_last+1, id_last+2)])
907 id_last += 4
909 # Top edge
910 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
911 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
912 m1x = min(m1x, area_width)
913 m2x = min(m2x, area_width)
914 if m1y > bottom_bar and m2y > bottom_bar:
915 vertices.extend([(m1x,m1y), (m2x,m1y),
916 (m2x,m1y+radius), (m1x,m1y+radius)])
917 indices.extend([(id_last, id_last+1, id_last+3),
918 (id_last+3, id_last+1, id_last+2)])
919 id_last += 4
921 # Right edge
922 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
923 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
924 m1y = max(m1y, bottom_bar)
925 m2y = max(m2y, bottom_bar)
926 if m1x < area_width and m2x < area_width:
927 vertices.extend([(m1x,m2y), (m1x+radius,m2y),
928 (m1x+radius,m1y), (m1x,m1y)])
929 indices.extend([(id_last, id_last+1, id_last+3),
930 (id_last+3, id_last+1, id_last+2)])
931 id_last += 4
933 # Bottom edge
934 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
935 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
936 m1x = min(m1x, area_width)
937 m2x = min(m2x, area_width)
938 if m1y > bottom_bar and m2y > bottom_bar:
939 vertices.extend([(m1x,m2y), (m2x,m2y),
940 (m2x,m1y-radius), (m1x,m1y-radius)])
941 indices.extend([(id_last, id_last+1, id_last+3),
942 (id_last+3, id_last+1, id_last+2)])
944 # now draw all edges in one batch
945 if len(vertices) != 0:
946 batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
947 shader.bind()
948 shader.uniform_float("color", colour)
949 batch.draw(shader)
951 def draw_callback_nodeoutline(self, context, mode):
952 if self.mouse_path:
954 bgl.glLineWidth(1)
955 bgl.glEnable(bgl.GL_BLEND)
956 bgl.glEnable(bgl.GL_LINE_SMOOTH)
957 bgl.glHint(bgl.GL_LINE_SMOOTH_HINT, bgl.GL_NICEST)
959 nodes, links = get_nodes_links(context)
961 shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
963 if mode == "LINK":
964 col_outer = (1.0, 0.2, 0.2, 0.4)
965 col_inner = (0.0, 0.0, 0.0, 0.5)
966 col_circle_inner = (0.3, 0.05, 0.05, 1.0)
967 elif mode == "LINKMENU":
968 col_outer = (0.4, 0.6, 1.0, 0.4)
969 col_inner = (0.0, 0.0, 0.0, 0.5)
970 col_circle_inner = (0.08, 0.15, .3, 1.0)
971 elif mode == "MIX":
972 col_outer = (0.2, 1.0, 0.2, 0.4)
973 col_inner = (0.0, 0.0, 0.0, 0.5)
974 col_circle_inner = (0.05, 0.3, 0.05, 1.0)
976 m1x = self.mouse_path[0][0]
977 m1y = self.mouse_path[0][1]
978 m2x = self.mouse_path[-1][0]
979 m2y = self.mouse_path[-1][1]
981 n1 = nodes[context.scene.NWLazySource]
982 n2 = nodes[context.scene.NWLazyTarget]
984 if n1 == n2:
985 col_outer = (0.4, 0.4, 0.4, 0.4)
986 col_inner = (0.0, 0.0, 0.0, 0.5)
987 col_circle_inner = (0.2, 0.2, 0.2, 1.0)
989 draw_rounded_node_border(shader, n1, radius=6, colour=col_outer) # outline
990 draw_rounded_node_border(shader, n1, radius=5, colour=col_inner) # inner
991 draw_rounded_node_border(shader, n2, radius=6, colour=col_outer) # outline
992 draw_rounded_node_border(shader, n2, radius=5, colour=col_inner) # inner
994 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
995 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
997 # circle outline
998 draw_circle_2d_filled(shader, m1x, m1y, 7, col_outer)
999 draw_circle_2d_filled(shader, m2x, m2y, 7, col_outer)
1001 # circle inner
1002 draw_circle_2d_filled(shader, m1x, m1y, 5, col_circle_inner)
1003 draw_circle_2d_filled(shader, m2x, m2y, 5, col_circle_inner)
1005 bgl.glDisable(bgl.GL_BLEND)
1006 bgl.glDisable(bgl.GL_LINE_SMOOTH)
1007 def get_active_tree(context):
1008 tree = context.space_data.node_tree
1009 path = []
1010 # Get nodes from currently edited tree.
1011 # If user is editing a group, space_data.node_tree is still the base level (outside group).
1012 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
1013 # the same as context.active_node, the user is in a group.
1014 # Check recursively until we find the real active node_tree:
1015 if tree.nodes.active:
1016 while tree.nodes.active != context.active_node:
1017 tree = tree.nodes.active.node_tree
1018 path.append(tree)
1019 return tree, path
1021 def get_nodes_links(context):
1022 tree, path = get_active_tree(context)
1023 return tree.nodes, tree.links
1025 def is_viewer_socket(socket):
1026 # checks if a internal socket is a valid viewer socket
1027 return socket.name == viewer_socket_name and socket.NWViewerSocket
1029 def get_internal_socket(socket):
1030 #get the internal socket from a socket inside or outside the group
1031 node = socket.node
1032 if node.type == 'GROUP_OUTPUT':
1033 source_iterator = node.inputs
1034 iterator = node.id_data.outputs
1035 elif node.type == 'GROUP_INPUT':
1036 source_iterator = node.outputs
1037 iterator = node.id_data.inputs
1038 elif hasattr(node, "node_tree"):
1039 if socket.is_output:
1040 source_iterator = node.outputs
1041 iterator = node.node_tree.outputs
1042 else:
1043 source_iterator = node.inputs
1044 iterator = node.node_tree.inputs
1045 else:
1046 return None
1048 for i, s in enumerate(source_iterator):
1049 if s == socket:
1050 break
1051 return iterator[i]
1053 def is_viewer_link(link, output_node):
1054 if "Emission Viewer" in link.to_node.name or link.to_node == output_node and link.to_socket == output_node.inputs[0]:
1055 return True
1056 if link.to_node.type == 'GROUP_OUTPUT':
1057 socket = get_internal_socket(link.to_socket)
1058 if is_viewer_socket(socket):
1059 return True
1060 return False
1062 def get_group_output_node(tree):
1063 for node in tree.nodes:
1064 if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
1065 return node
1067 def get_output_location(tree):
1068 # get right-most location
1069 sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
1070 max_xloc_node = sorted_by_xloc[-1]
1071 if max_xloc_node.name == 'Emission Viewer':
1072 max_xloc_node = sorted_by_xloc[-2]
1074 # get average y location
1075 sum_yloc = 0
1076 for node in tree.nodes:
1077 sum_yloc += node.location.y
1079 loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
1080 loc_y = sum_yloc / len(tree.nodes)
1081 return loc_x, loc_y
1083 # Principled prefs
1084 class NWPrincipledPreferences(bpy.types.PropertyGroup):
1085 base_color: StringProperty(
1086 name='Base Color',
1087 default='diffuse diff albedo base col color',
1088 description='Naming Components for Base Color maps')
1089 sss_color: StringProperty(
1090 name='Subsurface Color',
1091 default='sss subsurface',
1092 description='Naming Components for Subsurface Color maps')
1093 metallic: StringProperty(
1094 name='Metallic',
1095 default='metallic metalness metal mtl',
1096 description='Naming Components for metallness maps')
1097 specular: StringProperty(
1098 name='Specular',
1099 default='specularity specular spec spc',
1100 description='Naming Components for Specular maps')
1101 normal: StringProperty(
1102 name='Normal',
1103 default='normal nor nrm nrml norm',
1104 description='Naming Components for Normal maps')
1105 bump: StringProperty(
1106 name='Bump',
1107 default='bump bmp',
1108 description='Naming Components for bump maps')
1109 rough: StringProperty(
1110 name='Roughness',
1111 default='roughness rough rgh',
1112 description='Naming Components for roughness maps')
1113 gloss: StringProperty(
1114 name='Gloss',
1115 default='gloss glossy glossiness',
1116 description='Naming Components for glossy maps')
1117 displacement: StringProperty(
1118 name='Displacement',
1119 default='displacement displace disp dsp height heightmap',
1120 description='Naming Components for displacement maps')
1121 transmission: StringProperty(
1122 name='Transmission',
1123 default='transmission transparency',
1124 description='Naming Components for transmission maps')
1125 emission: StringProperty(
1126 name='Emission',
1127 default='emission emissive emit',
1128 description='Naming Components for emission maps')
1129 alpha: StringProperty(
1130 name='Alpha',
1131 default='alpha opacity',
1132 description='Naming Components for alpha maps')
1133 ambient_occlusion: StringProperty(
1134 name='Ambient Occlusion',
1135 default='ao ambient occlusion',
1136 description='Naming Components for AO maps')
1138 # Addon prefs
1139 class NWNodeWrangler(bpy.types.AddonPreferences):
1140 bl_idname = __name__
1142 merge_hide: EnumProperty(
1143 name="Hide Mix nodes",
1144 items=(
1145 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
1146 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
1147 ("NEVER", "Never", "Never collapse the new merge nodes")
1149 default='NON_SHADER',
1150 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
1151 merge_position: EnumProperty(
1152 name="Mix Node Position",
1153 items=(
1154 ("CENTER", "Center", "Place the Mix node between the two nodes"),
1155 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
1157 default='CENTER',
1158 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
1160 show_hotkey_list: BoolProperty(
1161 name="Show Hotkey List",
1162 default=False,
1163 description="Expand this box into a list of all the hotkeys for functions in this addon"
1165 hotkey_list_filter: StringProperty(
1166 name=" Filter by Name",
1167 default="",
1168 description="Show only hotkeys that have this text in their name",
1169 options={'TEXTEDIT_UPDATE'}
1171 show_principled_lists: BoolProperty(
1172 name="Show Principled naming tags",
1173 default=False,
1174 description="Expand this box into a list of all naming tags for principled texture setup"
1176 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
1178 def draw(self, context):
1179 layout = self.layout
1180 col = layout.column()
1181 col.prop(self, "merge_position")
1182 col.prop(self, "merge_hide")
1184 box = layout.box()
1185 col = box.column(align=True)
1186 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
1187 if self.show_principled_lists:
1188 tags = self.principled_tags
1190 col.prop(tags, "base_color")
1191 col.prop(tags, "sss_color")
1192 col.prop(tags, "metallic")
1193 col.prop(tags, "specular")
1194 col.prop(tags, "rough")
1195 col.prop(tags, "gloss")
1196 col.prop(tags, "normal")
1197 col.prop(tags, "bump")
1198 col.prop(tags, "displacement")
1199 col.prop(tags, "transmission")
1200 col.prop(tags, "emission")
1201 col.prop(tags, "alpha")
1202 col.prop(tags, "ambient_occlusion")
1204 box = layout.box()
1205 col = box.column(align=True)
1206 hotkey_button_name = "Show Hotkey List"
1207 if self.show_hotkey_list:
1208 hotkey_button_name = "Hide Hotkey List"
1209 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
1210 if self.show_hotkey_list:
1211 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
1212 col.separator()
1213 for hotkey in kmi_defs:
1214 if hotkey[7]:
1215 hotkey_name = hotkey[7]
1217 if self.hotkey_list_filter.lower() in hotkey_name.lower():
1218 row = col.row(align=True)
1219 row.label(text=hotkey_name)
1220 keystr = nice_hotkey_name(hotkey[1])
1221 if hotkey[4]:
1222 keystr = "Shift " + keystr
1223 if hotkey[5]:
1224 keystr = "Alt " + keystr
1225 if hotkey[3]:
1226 keystr = "Ctrl " + keystr
1227 row.label(text=keystr)
1231 def nw_check(context):
1232 space = context.space_data
1233 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
1235 valid = False
1236 if space.type == 'NODE_EDITOR' and space.node_tree is not None and space.tree_type in valid_trees:
1237 valid = True
1239 return valid
1241 class NWBase:
1242 @classmethod
1243 def poll(cls, context):
1244 return nw_check(context)
1247 # OPERATORS
1248 class NWLazyMix(Operator, NWBase):
1249 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
1250 bl_idname = "node.nw_lazy_mix"
1251 bl_label = "Mix Nodes"
1252 bl_options = {'REGISTER', 'UNDO'}
1254 def modal(self, context, event):
1255 context.area.tag_redraw()
1256 nodes, links = get_nodes_links(context)
1257 cont = True
1259 start_pos = [event.mouse_region_x, event.mouse_region_y]
1261 node1 = None
1262 if not context.scene.NWBusyDrawing:
1263 node1 = node_at_pos(nodes, context, event)
1264 if node1:
1265 context.scene.NWBusyDrawing = node1.name
1266 else:
1267 if context.scene.NWBusyDrawing != 'STOP':
1268 node1 = nodes[context.scene.NWBusyDrawing]
1270 context.scene.NWLazySource = node1.name
1271 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1273 if event.type == 'MOUSEMOVE':
1274 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1276 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1277 end_pos = [event.mouse_region_x, event.mouse_region_y]
1278 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1280 node2 = None
1281 node2 = node_at_pos(nodes, context, event)
1282 if node2:
1283 context.scene.NWBusyDrawing = node2.name
1285 if node1 == node2:
1286 cont = False
1288 if cont:
1289 if node1 and node2:
1290 for node in nodes:
1291 node.select = False
1292 node1.select = True
1293 node2.select = True
1295 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
1297 context.scene.NWBusyDrawing = ""
1298 return {'FINISHED'}
1300 elif event.type == 'ESC':
1301 print('cancelled')
1302 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1303 return {'CANCELLED'}
1305 return {'RUNNING_MODAL'}
1307 def invoke(self, context, event):
1308 if context.area.type == 'NODE_EDITOR':
1309 # the arguments we pass the the callback
1310 args = (self, context, 'MIX')
1311 # Add the region OpenGL drawing callback
1312 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1313 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1315 self.mouse_path = []
1317 context.window_manager.modal_handler_add(self)
1318 return {'RUNNING_MODAL'}
1319 else:
1320 self.report({'WARNING'}, "View3D not found, cannot run operator")
1321 return {'CANCELLED'}
1324 class NWLazyConnect(Operator, NWBase):
1325 """Connect two nodes without clicking a specific socket (automatically determined"""
1326 bl_idname = "node.nw_lazy_connect"
1327 bl_label = "Lazy Connect"
1328 bl_options = {'REGISTER', 'UNDO'}
1329 with_menu: BoolProperty()
1331 def modal(self, context, event):
1332 context.area.tag_redraw()
1333 nodes, links = get_nodes_links(context)
1334 cont = True
1336 start_pos = [event.mouse_region_x, event.mouse_region_y]
1338 node1 = None
1339 if not context.scene.NWBusyDrawing:
1340 node1 = node_at_pos(nodes, context, event)
1341 if node1:
1342 context.scene.NWBusyDrawing = node1.name
1343 else:
1344 if context.scene.NWBusyDrawing != 'STOP':
1345 node1 = nodes[context.scene.NWBusyDrawing]
1347 context.scene.NWLazySource = node1.name
1348 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1350 if event.type == 'MOUSEMOVE':
1351 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
1353 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
1354 end_pos = [event.mouse_region_x, event.mouse_region_y]
1355 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1357 node2 = None
1358 node2 = node_at_pos(nodes, context, event)
1359 if node2:
1360 context.scene.NWBusyDrawing = node2.name
1362 if node1 == node2:
1363 cont = False
1365 link_success = False
1366 if cont:
1367 if node1 and node2:
1368 original_sel = []
1369 original_unsel = []
1370 for node in nodes:
1371 if node.select == True:
1372 node.select = False
1373 original_sel.append(node)
1374 else:
1375 original_unsel.append(node)
1376 node1.select = True
1377 node2.select = True
1379 #link_success = autolink(node1, node2, links)
1380 if self.with_menu:
1381 if len(node1.outputs) > 1 and node2.inputs:
1382 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
1383 elif len(node1.outputs) == 1:
1384 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
1385 else:
1386 link_success = autolink(node1, node2, links)
1388 for node in original_sel:
1389 node.select = True
1390 for node in original_unsel:
1391 node.select = False
1393 if link_success:
1394 force_update(context)
1395 context.scene.NWBusyDrawing = ""
1396 return {'FINISHED'}
1398 elif event.type == 'ESC':
1399 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
1400 return {'CANCELLED'}
1402 return {'RUNNING_MODAL'}
1404 def invoke(self, context, event):
1405 if context.area.type == 'NODE_EDITOR':
1406 nodes, links = get_nodes_links(context)
1407 node = node_at_pos(nodes, context, event)
1408 if node:
1409 context.scene.NWBusyDrawing = node.name
1411 # the arguments we pass the the callback
1412 mode = "LINK"
1413 if self.with_menu:
1414 mode = "LINKMENU"
1415 args = (self, context, mode)
1416 # Add the region OpenGL drawing callback
1417 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1418 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1420 self.mouse_path = []
1422 context.window_manager.modal_handler_add(self)
1423 return {'RUNNING_MODAL'}
1424 else:
1425 self.report({'WARNING'}, "View3D not found, cannot run operator")
1426 return {'CANCELLED'}
1429 class NWDeleteUnused(Operator, NWBase):
1430 """Delete all nodes whose output is not used"""
1431 bl_idname = 'node.nw_del_unused'
1432 bl_label = 'Delete Unused Nodes'
1433 bl_options = {'REGISTER', 'UNDO'}
1435 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1436 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1438 def is_unused_node(self, node):
1439 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1440 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1441 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1442 if node.type in end_types:
1443 return False
1445 for output in node.outputs:
1446 if output.links:
1447 return False
1448 return True
1450 @classmethod
1451 def poll(cls, context):
1452 valid = False
1453 if nw_check(context):
1454 if context.space_data.node_tree.nodes:
1455 valid = True
1456 return valid
1458 def execute(self, context):
1459 nodes, links = get_nodes_links(context)
1461 # Store selection
1462 selection = []
1463 for node in nodes:
1464 if node.select == True:
1465 selection.append(node.name)
1467 for node in nodes:
1468 node.select = False
1470 deleted_nodes = []
1471 temp_deleted_nodes = []
1472 del_unused_iterations = len(nodes)
1473 for it in range(0, del_unused_iterations):
1474 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1475 for node in nodes:
1476 if self.is_unused_node(node):
1477 node.select = True
1478 deleted_nodes.append(node.name)
1479 bpy.ops.node.delete()
1481 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1482 break
1484 if self.delete_frames:
1485 repeat = True
1486 while repeat:
1487 frames_in_use = []
1488 frames = []
1489 repeat = False
1490 for node in nodes:
1491 if node.parent:
1492 frames_in_use.append(node.parent)
1493 for node in nodes:
1494 if node.type == 'FRAME' and node not in frames_in_use:
1495 frames.append(node)
1496 if node.parent:
1497 repeat = True # repeat for nested frames
1498 for node in frames:
1499 if node not in frames_in_use:
1500 node.select = True
1501 deleted_nodes.append(node.name)
1502 bpy.ops.node.delete()
1504 if self.delete_muted:
1505 for node in nodes:
1506 if node.mute:
1507 node.select = True
1508 deleted_nodes.append(node.name)
1509 bpy.ops.node.delete_reconnect()
1511 # get unique list of deleted nodes (iterations would count the same node more than once)
1512 deleted_nodes = list(set(deleted_nodes))
1513 for n in deleted_nodes:
1514 self.report({'INFO'}, "Node " + n + " deleted")
1515 num_deleted = len(deleted_nodes)
1516 n = ' node'
1517 if num_deleted > 1:
1518 n += 's'
1519 if num_deleted:
1520 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1521 else:
1522 self.report({'INFO'}, "Nothing deleted")
1524 # Restore selection
1525 nodes, links = get_nodes_links(context)
1526 for node in nodes:
1527 if node.name in selection:
1528 node.select = True
1529 return {'FINISHED'}
1531 def invoke(self, context, event):
1532 return context.window_manager.invoke_confirm(self, event)
1535 class NWSwapLinks(Operator, NWBase):
1536 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1537 bl_idname = 'node.nw_swap_links'
1538 bl_label = 'Swap Links'
1539 bl_options = {'REGISTER', 'UNDO'}
1541 @classmethod
1542 def poll(cls, context):
1543 valid = False
1544 if nw_check(context):
1545 if context.selected_nodes:
1546 valid = len(context.selected_nodes) <= 2
1547 return valid
1549 def execute(self, context):
1550 nodes, links = get_nodes_links(context)
1551 selected_nodes = context.selected_nodes
1552 n1 = selected_nodes[0]
1554 # Swap outputs
1555 if len(selected_nodes) == 2:
1556 n2 = selected_nodes[1]
1557 if n1.outputs and n2.outputs:
1558 n1_outputs = []
1559 n2_outputs = []
1561 out_index = 0
1562 for output in n1.outputs:
1563 if output.links:
1564 for link in output.links:
1565 n1_outputs.append([out_index, link.to_socket])
1566 links.remove(link)
1567 out_index += 1
1569 out_index = 0
1570 for output in n2.outputs:
1571 if output.links:
1572 for link in output.links:
1573 n2_outputs.append([out_index, link.to_socket])
1574 links.remove(link)
1575 out_index += 1
1577 for connection in n1_outputs:
1578 try:
1579 links.new(n2.outputs[connection[0]], connection[1])
1580 except:
1581 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1582 for connection in n2_outputs:
1583 try:
1584 links.new(n1.outputs[connection[0]], connection[1])
1585 except:
1586 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1587 else:
1588 if n1.outputs or n2.outputs:
1589 self.report({'WARNING'}, "One of the nodes has no outputs!")
1590 else:
1591 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1593 # Swap Inputs
1594 elif len(selected_nodes) == 1:
1595 if n1.inputs and n1.inputs[0].is_multi_input:
1596 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1597 return {'FINISHED'}
1598 if n1.inputs:
1599 types = []
1601 for i1 in n1.inputs:
1602 if i1.is_linked and not i1.is_multi_input:
1603 similar_types = 0
1604 for i2 in n1.inputs:
1605 if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
1606 similar_types += 1
1607 types.append ([i1, similar_types, i])
1608 i += 1
1609 types.sort(key=lambda k: k[1], reverse=True)
1611 if types:
1612 t = types[0]
1613 if t[1] == 2:
1614 for i2 in n1.inputs:
1615 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1616 pair = [t[0], i2]
1617 i1f = pair[0].links[0].from_socket
1618 i1t = pair[0].links[0].to_socket
1619 i2f = pair[1].links[0].from_socket
1620 i2t = pair[1].links[0].to_socket
1621 links.new(i1f, i2t)
1622 links.new(i2f, i1t)
1623 if t[1] == 1:
1624 if len(types) == 1:
1625 fs = t[0].links[0].from_socket
1626 i = t[2]
1627 links.remove(t[0].links[0])
1628 if i+1 == len(n1.inputs):
1629 i = -1
1630 i += 1
1631 while n1.inputs[i].is_linked:
1632 i += 1
1633 links.new(fs, n1.inputs[i])
1634 elif len(types) == 2:
1635 i1f = types[0][0].links[0].from_socket
1636 i1t = types[0][0].links[0].to_socket
1637 i2f = types[1][0].links[0].from_socket
1638 i2t = types[1][0].links[0].to_socket
1639 links.new(i1f, i2t)
1640 links.new(i2f, i1t)
1642 else:
1643 self.report({'WARNING'}, "This node has no input connections to swap!")
1644 else:
1645 self.report({'WARNING'}, "This node has no inputs to swap!")
1647 force_update(context)
1648 return {'FINISHED'}
1651 class NWResetBG(Operator, NWBase):
1652 """Reset the zoom and position of the background image"""
1653 bl_idname = 'node.nw_bg_reset'
1654 bl_label = 'Reset Backdrop'
1655 bl_options = {'REGISTER', 'UNDO'}
1657 @classmethod
1658 def poll(cls, context):
1659 valid = False
1660 if nw_check(context):
1661 snode = context.space_data
1662 valid = snode.tree_type == 'CompositorNodeTree'
1663 return valid
1665 def execute(self, context):
1666 context.space_data.backdrop_zoom = 1
1667 context.space_data.backdrop_offset[0] = 0
1668 context.space_data.backdrop_offset[1] = 0
1669 return {'FINISHED'}
1672 class NWAddAttrNode(Operator, NWBase):
1673 """Add an Attribute node with this name"""
1674 bl_idname = 'node.nw_add_attr_node'
1675 bl_label = 'Add UV map'
1676 bl_options = {'REGISTER', 'UNDO'}
1678 attr_name: StringProperty()
1680 def execute(self, context):
1681 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1682 nodes, links = get_nodes_links(context)
1683 nodes.active.attribute_name = self.attr_name
1684 return {'FINISHED'}
1686 class NWPreviewNode(Operator, NWBase):
1687 bl_idname = "node.nw_preview_node"
1688 bl_label = "Preview Node"
1689 bl_description = "Connect active node to Emission Shader for shadeless previews, or to the geometry node tree's output"
1690 bl_options = {'REGISTER', 'UNDO'}
1692 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1693 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1694 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1695 run_in_geometry_nodes: BoolProperty(default=True)
1697 def __init__(self):
1698 self.shader_output_type = ""
1699 self.shader_output_ident = ""
1700 self.shader_viewer_ident = ""
1702 @classmethod
1703 def poll(cls, context):
1704 if nw_check(context):
1705 space = context.space_data
1706 if space.tree_type == 'ShaderNodeTree' or space.tree_type == 'GeometryNodeTree':
1707 if context.active_node:
1708 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1709 return True
1710 else:
1711 return True
1712 return False
1714 def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
1715 #check if a viewer output already exists in a node group otherwise create
1716 if hasattr(node, "node_tree"):
1717 index = None
1718 if len(node.node_tree.outputs):
1719 free_socket = None
1720 for i, socket in enumerate(node.node_tree.outputs):
1721 if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
1722 #if viewer output is already used but leads to the same socket we can still use it
1723 is_used = self.is_socket_used_other_mats(socket)
1724 if is_used:
1725 if connect_socket == None:
1726 continue
1727 groupout = get_group_output_node(node.node_tree)
1728 groupout_input = groupout.inputs[i]
1729 links = groupout_input.links
1730 if connect_socket not in [link.from_socket for link in links]:
1731 continue
1732 index=i
1733 break
1734 if not free_socket:
1735 free_socket = i
1736 if not index and free_socket:
1737 index = free_socket
1739 if not index:
1740 #create viewer socket
1741 node.node_tree.outputs.new(socket_type, viewer_socket_name)
1742 index = len(node.node_tree.outputs) - 1
1743 node.node_tree.outputs[index].NWViewerSocket = True
1744 return index
1746 def init_shader_variables(self, space, shader_type):
1747 if shader_type == 'OBJECT':
1748 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1749 self.shader_output_type = "OUTPUT_MATERIAL"
1750 self.shader_output_ident = "ShaderNodeOutputMaterial"
1751 self.shader_viewer_ident = "ShaderNodeEmission"
1752 else:
1753 self.shader_output_type = "OUTPUT_LIGHT"
1754 self.shader_output_ident = "ShaderNodeOutputLight"
1755 self.shader_viewer_ident = "ShaderNodeEmission"
1757 elif shader_type == 'WORLD':
1758 self.shader_output_type = "OUTPUT_WORLD"
1759 self.shader_output_ident = "ShaderNodeOutputWorld"
1760 self.shader_viewer_ident = "ShaderNodeBackground"
1762 def get_shader_output_node(self, tree):
1763 for node in tree.nodes:
1764 if node.type == self.shader_output_type and node.is_active_output == True:
1765 return node
1767 @classmethod
1768 def ensure_group_output(cls, tree):
1769 #check if a group output node exists otherwise create
1770 groupout = get_group_output_node(tree)
1771 if not groupout:
1772 groupout = tree.nodes.new('NodeGroupOutput')
1773 loc_x, loc_y = get_output_location(tree)
1774 groupout.location.x = loc_x
1775 groupout.location.y = loc_y
1776 groupout.select = False
1777 # So that we don't keep on adding new group outputs
1778 groupout.is_active_output = True
1779 return groupout
1781 @classmethod
1782 def search_sockets(cls, node, sockets, index=None):
1783 # recursively scan nodes for viewer sockets and store in list
1784 for i, input_socket in enumerate(node.inputs):
1785 if index and i != index:
1786 continue
1787 if len(input_socket.links):
1788 link = input_socket.links[0]
1789 next_node = link.from_node
1790 external_socket = link.from_socket
1791 if hasattr(next_node, "node_tree"):
1792 for socket_index, s in enumerate(next_node.outputs):
1793 if s == external_socket:
1794 break
1795 socket = next_node.node_tree.outputs[socket_index]
1796 if is_viewer_socket(socket) and socket not in sockets:
1797 sockets.append(socket)
1798 #continue search inside of node group but restrict socket to where we came from
1799 groupout = get_group_output_node(next_node.node_tree)
1800 cls.search_sockets(groupout, sockets, index=socket_index)
1802 @classmethod
1803 def scan_nodes(cls, tree, sockets):
1804 # get all viewer sockets in a material tree
1805 for node in tree.nodes:
1806 if hasattr(node, "node_tree"):
1807 for socket in node.node_tree.outputs:
1808 if is_viewer_socket(socket) and (socket not in sockets):
1809 sockets.append(socket)
1810 cls.scan_nodes(node.node_tree, sockets)
1812 def link_leads_to_used_socket(self, link):
1813 #return True if link leads to a socket that is already used in this material
1814 socket = get_internal_socket(link.to_socket)
1815 return (socket and self.is_socket_used_active_mat(socket))
1817 def is_socket_used_active_mat(self, socket):
1818 #ensure used sockets in active material is calculated and check given socket
1819 if not hasattr(self, "used_viewer_sockets_active_mat"):
1820 self.used_viewer_sockets_active_mat = []
1821 materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
1822 if materialout:
1823 emission = self.get_viewer_node(materialout)
1824 self.search_sockets((emission if emission else materialout), self.used_viewer_sockets_active_mat)
1825 return socket in self.used_viewer_sockets_active_mat
1827 def is_socket_used_other_mats(self, socket):
1828 #ensure used sockets in other materials are calculated and check given socket
1829 if not hasattr(self, "used_viewer_sockets_other_mats"):
1830 self.used_viewer_sockets_other_mats = []
1831 for mat in bpy.data.materials:
1832 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
1833 continue
1834 # get viewer node
1835 materialout = self.get_shader_output_node(mat.node_tree)
1836 if materialout:
1837 emission = self.get_viewer_node(materialout)
1838 self.search_sockets((emission if emission else materialout), self.used_viewer_sockets_other_mats)
1839 return socket in self.used_viewer_sockets_other_mats
1841 @staticmethod
1842 def get_viewer_node(materialout):
1843 input_socket = materialout.inputs[0]
1844 if len(input_socket.links) > 0:
1845 node = input_socket.links[0].from_node
1846 if node.type == 'EMISSION' and node.name == "Emission Viewer":
1847 return node
1849 def invoke(self, context, event):
1850 space = context.space_data
1851 # Ignore operator when running in wrong context.
1852 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
1853 return {'PASS_THROUGH'}
1855 shader_type = space.shader_type
1856 self.init_shader_variables(space, shader_type)
1857 shader_types = [x[1] for x in shaders_shader_nodes_props]
1858 mlocx = event.mouse_region_x
1859 mlocy = event.mouse_region_y
1860 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
1861 if 'FINISHED' in select_node: # only run if mouse click is on a node
1862 active_tree, path_to_tree = get_active_tree(context)
1863 nodes, links = active_tree.nodes, active_tree.links
1864 base_node_tree = space.node_tree
1865 active = nodes.active
1867 # For geometry node trees we just connect to the group output,
1868 # because there is no "viewer node" yet.
1869 if space.tree_type == "GeometryNodeTree":
1870 valid = False
1871 if active:
1872 for out in active.outputs:
1873 if is_visible_socket(out):
1874 valid = True
1875 break
1876 # Exit early
1877 if not valid:
1878 return {'FINISHED'}
1880 delete_sockets = []
1882 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1883 self.scan_nodes(base_node_tree, delete_sockets)
1885 # Find (or create if needed) the output of this node tree
1886 geometryoutput = self.ensure_group_output(base_node_tree)
1888 # Analyze outputs, make links
1889 out_i = None
1890 valid_outputs = []
1891 for i, out in enumerate(active.outputs):
1892 if is_visible_socket(out) and out.type == 'GEOMETRY':
1893 valid_outputs.append(i)
1894 if valid_outputs:
1895 out_i = valid_outputs[0] # Start index of node's outputs
1896 for i, valid_i in enumerate(valid_outputs):
1897 for out_link in active.outputs[valid_i].links:
1898 if is_viewer_link(out_link, geometryoutput):
1899 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1900 if i < len(valid_outputs) - 1:
1901 out_i = valid_outputs[i + 1]
1902 else:
1903 out_i = valid_outputs[0]
1905 make_links = [] # store sockets for new links
1906 delete_nodes = [] # store unused nodes to delete in the end
1907 if active.outputs:
1908 # If there is no 'GEOMETRY' output type - We can't preview the node
1909 if out_i is None:
1910 return {'FINISHED'}
1911 socket_type = 'GEOMETRY'
1912 # Find an input socket of the output of type geometry
1913 geometryoutindex = None
1914 for i,inp in enumerate(geometryoutput.inputs):
1915 if inp.type == socket_type:
1916 geometryoutindex = i
1917 break
1918 if geometryoutindex is None:
1919 # Create geometry socket
1920 geometryoutput.inputs.new(socket_type, 'Geometry')
1921 geometryoutindex = len(geometryoutput.inputs) - 1
1923 make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
1924 output_socket = geometryoutput.inputs[geometryoutindex]
1925 for li_from, li_to in make_links:
1926 base_node_tree.links.new(li_from, li_to)
1927 tree = base_node_tree
1928 link_end = output_socket
1929 while tree.nodes.active != active:
1930 node = tree.nodes.active
1931 index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1932 link_start = node.outputs[index]
1933 node_socket = node.node_tree.outputs[index]
1934 if node_socket in delete_sockets:
1935 delete_sockets.remove(node_socket)
1936 tree.links.new(link_start, link_end)
1937 # Iterate
1938 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1939 tree = tree.nodes.active.node_tree
1940 tree.links.new(active.outputs[out_i], link_end)
1942 # Delete sockets
1943 for socket in delete_sockets:
1944 tree = socket.id_data
1945 tree.outputs.remove(socket)
1947 # Delete nodes
1948 for tree, node in delete_nodes:
1949 tree.nodes.remove(node)
1951 nodes.active = active
1952 active.select = True
1953 force_update(context)
1954 return {'FINISHED'}
1957 # What follows is code for the shader editor
1958 output_types = [x[1] for x in shaders_output_nodes_props]
1959 valid = False
1960 if active:
1961 if (active.name != "Emission Viewer") and (active.type not in output_types):
1962 for out in active.outputs:
1963 if is_visible_socket(out):
1964 valid = True
1965 break
1966 if valid:
1967 # get material_output node
1968 materialout = None # placeholder node
1969 delete_sockets = []
1971 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1972 self.scan_nodes(base_node_tree, delete_sockets)
1974 materialout = self.get_shader_output_node(base_node_tree)
1975 if not materialout:
1976 materialout = base_node_tree.nodes.new(self.shader_output_ident)
1977 materialout.location = get_output_location(base_node_tree)
1978 materialout.select = False
1979 # Analyze outputs, add "Emission Viewer" if needed, make links
1980 out_i = None
1981 valid_outputs = []
1982 for i, out in enumerate(active.outputs):
1983 if is_visible_socket(out):
1984 valid_outputs.append(i)
1985 if valid_outputs:
1986 out_i = valid_outputs[0] # Start index of node's outputs
1987 for i, valid_i in enumerate(valid_outputs):
1988 for out_link in active.outputs[valid_i].links:
1989 if is_viewer_link(out_link, materialout):
1990 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1991 if i < len(valid_outputs) - 1:
1992 out_i = valid_outputs[i + 1]
1993 else:
1994 out_i = valid_outputs[0]
1996 make_links = [] # store sockets for new links
1997 delete_nodes = [] # store unused nodes to delete in the end
1998 if active.outputs:
1999 # If output type not 'SHADER' - "Emission Viewer" needed
2000 if active.outputs[out_i].type != 'SHADER':
2001 socket_type = 'NodeSocketColor'
2002 # get Emission Viewer node
2003 emission_exists = False
2004 emission_placeholder = base_node_tree.nodes[0]
2005 for node in base_node_tree.nodes:
2006 if "Emission Viewer" in node.name:
2007 emission_exists = True
2008 emission_placeholder = node
2009 if not emission_exists:
2010 emission = base_node_tree.nodes.new(self.shader_viewer_ident)
2011 emission.hide = True
2012 emission.location = [materialout.location.x, (materialout.location.y + 40)]
2013 emission.label = "Viewer"
2014 emission.name = "Emission Viewer"
2015 emission.use_custom_color = True
2016 emission.color = (0.6, 0.5, 0.4)
2017 emission.select = False
2018 else:
2019 emission = emission_placeholder
2020 output_socket = emission.inputs[0]
2022 # If Viewer is connected to output by user, don't change those connections (patch by gandalf3)
2023 if emission.outputs[0].links.__len__() > 0:
2024 if not emission.outputs[0].links[0].to_node == materialout:
2025 make_links.append((emission.outputs[0], materialout.inputs[0]))
2026 else:
2027 make_links.append((emission.outputs[0], materialout.inputs[0]))
2029 # Set brightness of viewer to compensate for Film and CM exposure
2030 if context.scene.render.engine == 'CYCLES' and hasattr(context.scene, 'cycles'):
2031 intensity = 1/context.scene.cycles.film_exposure # Film exposure is a multiplier
2032 else:
2033 intensity = 1
2035 intensity /= pow(2, (context.scene.view_settings.exposure)) # CM exposure is measured in stops/EVs (2^x)
2036 emission.inputs[1].default_value = intensity
2038 else:
2039 # Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
2040 socket_type = 'NodeSocketShader'
2041 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
2042 make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
2043 output_socket = materialout.inputs[materialout_index]
2044 for node in base_node_tree.nodes:
2045 if node.name == 'Emission Viewer':
2046 delete_nodes.append((base_node_tree, node))
2047 for li_from, li_to in make_links:
2048 base_node_tree.links.new(li_from, li_to)
2050 # Create links through node groups until we reach the active node
2051 tree = base_node_tree
2052 link_end = output_socket
2053 while tree.nodes.active != active:
2054 node = tree.nodes.active
2055 index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
2056 link_start = node.outputs[index]
2057 node_socket = node.node_tree.outputs[index]
2058 if node_socket in delete_sockets:
2059 delete_sockets.remove(node_socket)
2060 tree.links.new(link_start, link_end)
2061 # Iterate
2062 link_end = self.ensure_group_output(node.node_tree).inputs[index]
2063 tree = tree.nodes.active.node_tree
2064 tree.links.new(active.outputs[out_i], link_end)
2066 # Delete sockets
2067 for socket in delete_sockets:
2068 if not self.is_socket_used_other_mats(socket):
2069 tree = socket.id_data
2070 tree.outputs.remove(socket)
2072 # Delete nodes
2073 for tree, node in delete_nodes:
2074 tree.nodes.remove(node)
2076 nodes.active = active
2077 active.select = True
2079 force_update(context)
2081 return {'FINISHED'}
2082 else:
2083 return {'CANCELLED'}
2086 class NWFrameSelected(Operator, NWBase):
2087 bl_idname = "node.nw_frame_selected"
2088 bl_label = "Frame Selected"
2089 bl_description = "Add a frame node and parent the selected nodes to it"
2090 bl_options = {'REGISTER', 'UNDO'}
2092 label_prop: StringProperty(
2093 name='Label',
2094 description='The visual name of the frame node',
2095 default=' '
2097 color_prop: FloatVectorProperty(
2098 name="Color",
2099 description="The color of the frame node",
2100 default=(0.6, 0.6, 0.6),
2101 min=0, max=1, step=1, precision=3,
2102 subtype='COLOR_GAMMA', size=3
2105 def execute(self, context):
2106 nodes, links = get_nodes_links(context)
2107 selected = []
2108 for node in nodes:
2109 if node.select == True:
2110 selected.append(node)
2112 bpy.ops.node.add_node(type='NodeFrame')
2113 frm = nodes.active
2114 frm.label = self.label_prop
2115 frm.use_custom_color = True
2116 frm.color = self.color_prop
2118 for node in selected:
2119 node.parent = frm
2121 return {'FINISHED'}
2124 class NWReloadImages(Operator):
2125 bl_idname = "node.nw_reload_images"
2126 bl_label = "Reload Images"
2127 bl_description = "Update all the image nodes to match their files on disk"
2129 @classmethod
2130 def poll(cls, context):
2131 valid = False
2132 if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
2133 if context.active_node is not None:
2134 for out in context.active_node.outputs:
2135 if is_visible_socket(out):
2136 valid = True
2137 break
2138 return valid
2140 def execute(self, context):
2141 nodes, links = get_nodes_links(context)
2142 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
2143 num_reloaded = 0
2144 for node in nodes:
2145 if node.type in image_types:
2146 if node.type == "TEXTURE":
2147 if node.texture: # node has texture assigned
2148 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
2149 if node.texture.image: # texture has image assigned
2150 node.texture.image.reload()
2151 num_reloaded += 1
2152 else:
2153 if node.image:
2154 node.image.reload()
2155 num_reloaded += 1
2157 if num_reloaded:
2158 self.report({'INFO'}, "Reloaded images")
2159 print("Reloaded " + str(num_reloaded) + " images")
2160 force_update(context)
2161 return {'FINISHED'}
2162 else:
2163 self.report({'WARNING'}, "No images found to reload in this node tree")
2164 return {'CANCELLED'}
2167 class NWSwitchNodeType(Operator, NWBase):
2168 """Switch type of selected nodes """
2169 bl_idname = "node.nw_swtch_node_type"
2170 bl_label = "Switch Node Type"
2171 bl_options = {'REGISTER', 'UNDO'}
2173 to_type: EnumProperty(
2174 name="Switch to type",
2175 items=list(shaders_input_nodes_props) +
2176 list(shaders_output_nodes_props) +
2177 list(shaders_shader_nodes_props) +
2178 list(shaders_texture_nodes_props) +
2179 list(shaders_color_nodes_props) +
2180 list(shaders_vector_nodes_props) +
2181 list(shaders_converter_nodes_props) +
2182 list(shaders_layout_nodes_props) +
2183 list(compo_input_nodes_props) +
2184 list(compo_output_nodes_props) +
2185 list(compo_color_nodes_props) +
2186 list(compo_converter_nodes_props) +
2187 list(compo_filter_nodes_props) +
2188 list(compo_vector_nodes_props) +
2189 list(compo_matte_nodes_props) +
2190 list(compo_distort_nodes_props) +
2191 list(compo_layout_nodes_props) +
2192 list(blender_mat_input_nodes_props) +
2193 list(blender_mat_output_nodes_props) +
2194 list(blender_mat_color_nodes_props) +
2195 list(blender_mat_vector_nodes_props) +
2196 list(blender_mat_converter_nodes_props) +
2197 list(blender_mat_layout_nodes_props) +
2198 list(texture_input_nodes_props) +
2199 list(texture_output_nodes_props) +
2200 list(texture_color_nodes_props) +
2201 list(texture_pattern_nodes_props) +
2202 list(texture_textures_nodes_props) +
2203 list(texture_converter_nodes_props) +
2204 list(texture_distort_nodes_props) +
2205 list(texture_layout_nodes_props)
2208 geo_to_type: StringProperty(
2209 name="Switch to type",
2210 default = '',
2213 def execute(self, context):
2214 nodes, links = get_nodes_links(context)
2215 to_type = self.to_type
2216 if self.geo_to_type != '':
2217 to_type = self.geo_to_type
2218 # Those types of nodes will not swap.
2219 src_excludes = ('NodeFrame')
2220 # Those attributes of nodes will be copied if possible
2221 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
2222 'show_options', 'show_preview', 'show_texture',
2223 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
2225 selected = [n for n in nodes if n.select]
2226 reselect = []
2227 for node in [n for n in selected if
2228 n.rna_type.identifier not in src_excludes and
2229 n.rna_type.identifier != to_type]:
2230 new_node = nodes.new(to_type)
2231 for attr in attrs_to_pass:
2232 if hasattr(node, attr) and hasattr(new_node, attr):
2233 setattr(new_node, attr, getattr(node, attr))
2234 # set image datablock of dst to image of src
2235 if hasattr(node, 'image') and hasattr(new_node, 'image'):
2236 if node.image:
2237 new_node.image = node.image
2238 # Special cases
2239 if new_node.type == 'SWITCH':
2240 new_node.hide = True
2241 # Dictionaries: src_sockets and dst_sockets:
2242 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
2243 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
2244 # in 'INPUTS' and 'OUTPUTS':
2245 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
2246 # socket entry:
2247 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2248 src_sockets = {
2249 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2250 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2252 dst_sockets = {
2253 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2254 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
2256 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
2257 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
2258 # check src node to set src_sockets values and dst node to set dst_sockets dict values
2259 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
2260 # Check node's inputs and outputs and fill proper entries in "sockets" dict
2261 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
2262 # enumerate in inputs, then in outputs
2263 # find name, default value and links of socket
2264 for i, socket in enumerate(in_out):
2265 the_name = socket.name
2266 dval = None
2267 # Not every socket, especially in outputs has "default_value"
2268 if hasattr(socket, 'default_value'):
2269 dval = socket.default_value
2270 socket_links = []
2271 for lnk in socket.links:
2272 socket_links.append(lnk)
2273 # check type of socket to fill proper keys.
2274 for the_type in types_order_one:
2275 if socket.type == the_type:
2276 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
2277 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
2278 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
2279 # Check which of the types in inputs/outputs is considered to be "main".
2280 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
2281 for type_check in types_order_one:
2282 if sockets[in_out_name][type_check]:
2283 sockets[in_out_name]['MAIN'] = type_check
2284 break
2286 matches = {
2287 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2288 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
2291 for inout, soctype in (
2292 ('INPUTS', 'MAIN',),
2293 ('INPUTS', 'SHADER',),
2294 ('INPUTS', 'RGBA',),
2295 ('INPUTS', 'VECTOR',),
2296 ('INPUTS', 'VALUE',),
2297 ('OUTPUTS', 'MAIN',),
2298 ('OUTPUTS', 'SHADER',),
2299 ('OUTPUTS', 'RGBA',),
2300 ('OUTPUTS', 'VECTOR',),
2301 ('OUTPUTS', 'VALUE',),
2303 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
2304 if soctype == 'MAIN':
2305 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
2306 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
2307 else:
2308 sc = src_sockets[inout][soctype]
2309 dt = dst_sockets[inout][soctype]
2310 # start with 'dt' to determine number of possibilities.
2311 for i, soc in enumerate(dt):
2312 # if src main has enough entries - match them with dst main sockets by indexes.
2313 if len(sc) > i:
2314 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
2315 # add 'VALUE_NAME' criterion to inputs.
2316 if inout == 'INPUTS' and soctype == 'VALUE':
2317 for s in sc:
2318 if s[2] == soc[2]: # if names match
2319 # append src (index, dval), dst (index, dval)
2320 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
2322 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
2323 # This creates better links when relinking textures.
2324 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
2325 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
2327 # Pass default values and RELINK:
2328 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
2329 # INPUTS: Base on matches in proper order.
2330 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
2331 # pass dvals
2332 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
2333 new_node.inputs[dst_i].default_value = src_dval
2334 # Special case: switch to math
2335 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2336 new_node.type == 'MATH' and\
2337 tp == 'MAIN':
2338 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
2339 new_node.inputs[dst_i].default_value = new_dst_dval
2340 if node.type == 'MIX_RGB':
2341 if node.blend_type in [o[0] for o in operations]:
2342 new_node.operation = node.blend_type
2343 # Special case: switch from math to some types
2344 if node.type == 'MATH' and\
2345 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
2346 tp == 'MAIN':
2347 for i in range(3):
2348 new_node.inputs[dst_i].default_value[i] = src_dval
2349 if new_node.type == 'MIX_RGB':
2350 if node.operation in [t[0] for t in blend_types]:
2351 new_node.blend_type = node.operation
2352 # Set Fac of MIX_RGB to 1.0
2353 new_node.inputs[0].default_value = 1.0
2354 # make link only when dst matching input is not linked already.
2355 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
2356 in_src_link = node.inputs[src_i].links[0]
2357 in_dst_socket = new_node.inputs[dst_i]
2358 links.new(in_src_link.from_socket, in_dst_socket)
2359 links.remove(in_src_link)
2360 # OUTPUTS: Base on matches in proper order.
2361 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
2362 for out_src_link in node.outputs[src_i].links:
2363 out_dst_socket = new_node.outputs[dst_i]
2364 links.new(out_dst_socket, out_src_link.to_socket)
2365 # relink rest inputs if possible, no criteria
2366 for src_inp in node.inputs:
2367 for dst_inp in new_node.inputs:
2368 if src_inp.links and not dst_inp.links:
2369 src_link = src_inp.links[0]
2370 links.new(src_link.from_socket, dst_inp)
2371 links.remove(src_link)
2372 # relink rest outputs if possible, base on node kind if any left.
2373 for src_o in node.outputs:
2374 for out_src_link in src_o.links:
2375 for dst_o in new_node.outputs:
2376 if src_o.type == dst_o.type:
2377 links.new(dst_o, out_src_link.to_socket)
2378 # relink rest outputs no criteria if any left. Link all from first output.
2379 for src_o in node.outputs:
2380 for out_src_link in src_o.links:
2381 if new_node.outputs:
2382 links.new(new_node.outputs[0], out_src_link.to_socket)
2383 nodes.remove(node)
2384 force_update(context)
2385 return {'FINISHED'}
2388 class NWMergeNodes(Operator, NWBase):
2389 bl_idname = "node.nw_merge_nodes"
2390 bl_label = "Merge Nodes"
2391 bl_description = "Merge Selected Nodes"
2392 bl_options = {'REGISTER', 'UNDO'}
2394 mode: EnumProperty(
2395 name="mode",
2396 description="All possible blend types, boolean operations and math operations",
2397 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],
2399 merge_type: EnumProperty(
2400 name="merge type",
2401 description="Type of Merge to be used",
2402 items=(
2403 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
2404 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
2405 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
2406 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
2407 ('MATH', 'Math Node', 'Merge using Math Nodes'),
2408 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
2409 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
2413 # Check if the link connects to a node that is in selected_nodes
2414 # If not, then check recursively for each link in the nodes outputs.
2415 # If yes, return True. If the recursion stops without finding a node
2416 # in selected_nodes, it returns False. The depth is used to prevent
2417 # getting stuck in a loop because of an already present cycle.
2418 @staticmethod
2419 def link_creates_cycle(link, selected_nodes, depth=0)->bool:
2420 if depth > 255:
2421 # We're stuck in a cycle, but that cycle was already present,
2422 # so we return False.
2423 # NOTE: The number 255 is arbitrary, but seems to work well.
2424 return False
2425 node = link.to_node
2426 if node in selected_nodes:
2427 return True
2428 if not node.outputs:
2429 return False
2430 for output in node.outputs:
2431 if output.is_linked:
2432 for olink in output.links:
2433 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1):
2434 return True
2435 # None of the outputs found a node in selected_nodes, so there is no cycle.
2436 return False
2438 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
2439 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
2440 # be connected. The last one is assumed to be a multi input socket.
2441 # For convenience the node is returned.
2442 @staticmethod
2443 def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices):
2444 # The y-location of the last node
2445 loc_y = nodes_list[-1][2]
2446 if merge_position == 'CENTER':
2447 # Average the y-location
2448 for i in range(len(nodes_list)-1):
2449 loc_y += nodes_list[i][2]
2450 loc_y = loc_y/len(nodes_list)
2451 new_node = nodes.new(node_name)
2452 new_node.hide = do_hide
2453 new_node.location.x = loc_x
2454 new_node.location.y = loc_y
2455 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
2456 prev_links = []
2457 outputs_for_multi_input = []
2458 for i,node in enumerate(selected_nodes):
2459 node.select = False
2460 # Search for the first node which had output links that do not create
2461 # a cycle, which we can then reconnect afterwards.
2462 if prev_links == [] and node.outputs[0].is_linked:
2463 prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)]
2464 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
2465 # To get the placement to look right we need to reverse the order in which we connect the
2466 # outputs to the multi input socket.
2467 if i < len(socket_indices) - 1:
2468 ind = socket_indices[i]
2469 links.new(node.outputs[0], new_node.inputs[ind])
2470 else:
2471 outputs_for_multi_input.insert(0, node.outputs[0])
2472 if outputs_for_multi_input != []:
2473 ind = socket_indices[-1]
2474 for output in outputs_for_multi_input:
2475 links.new(output, new_node.inputs[ind])
2476 if prev_links != []:
2477 for link in prev_links:
2478 links.new(new_node.outputs[0], link.to_node.inputs[0])
2479 return new_node
2481 def execute(self, context):
2482 settings = context.preferences.addons[__name__].preferences
2483 merge_hide = settings.merge_hide
2484 merge_position = settings.merge_position # 'center' or 'bottom'
2486 do_hide = False
2487 do_hide_shader = False
2488 if merge_hide == 'ALWAYS':
2489 do_hide = True
2490 do_hide_shader = True
2491 elif merge_hide == 'NON_SHADER':
2492 do_hide = True
2494 tree_type = context.space_data.node_tree.type
2495 if tree_type == 'GEOMETRY':
2496 node_type = 'GeometryNode'
2497 if tree_type == 'COMPOSITING':
2498 node_type = 'CompositorNode'
2499 elif tree_type == 'SHADER':
2500 node_type = 'ShaderNode'
2501 elif tree_type == 'TEXTURE':
2502 node_type = 'TextureNode'
2503 nodes, links = get_nodes_links(context)
2504 mode = self.mode
2505 merge_type = self.merge_type
2506 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2507 # 'ZCOMBINE' works only if mode == 'MIX'
2508 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2509 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2510 merge_type = 'MIX'
2511 mode = 'MIX'
2512 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
2513 merge_type = 'AUTO'
2514 # The math nodes used for geometry nodes are of type 'ShaderNode'
2515 if merge_type == 'MATH' and tree_type == 'GEOMETRY':
2516 node_type = 'ShaderNode'
2517 selected_mix = [] # entry = [index, loc]
2518 selected_shader = [] # entry = [index, loc]
2519 selected_geometry = [] # entry = [index, loc]
2520 selected_math = [] # entry = [index, loc]
2521 selected_vector = [] # entry = [index, loc]
2522 selected_z = [] # entry = [index, loc]
2523 selected_alphaover = [] # entry = [index, loc]
2525 for i, node in enumerate(nodes):
2526 if node.select and node.outputs:
2527 if merge_type == 'AUTO':
2528 for (type, types_list, dst) in (
2529 ('SHADER', ('MIX', 'ADD'), selected_shader),
2530 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2531 ('RGBA', [t[0] for t in blend_types], selected_mix),
2532 ('VALUE', [t[0] for t in operations], selected_math),
2533 ('VECTOR', [], selected_vector),
2535 output_type = node.outputs[0].type
2536 valid_mode = mode in types_list
2537 # When mode is 'MIX' we have to cheat since the mix node is not used in
2538 # geometry nodes.
2539 if tree_type == 'GEOMETRY':
2540 if mode == 'MIX':
2541 if output_type == 'VALUE' and type == 'VALUE':
2542 valid_mode = True
2543 elif output_type == 'VECTOR' and type == 'VECTOR':
2544 valid_mode = True
2545 elif type == 'GEOMETRY':
2546 valid_mode = True
2547 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2548 # Cheat that output type is 'RGBA',
2549 # and that 'MIX' exists in math operations list.
2550 # This way when selected_mix list is analyzed:
2551 # Node data will be appended even though it doesn't meet requirements.
2552 elif output_type != 'SHADER' and mode == 'MIX':
2553 output_type = 'RGBA'
2554 valid_mode = True
2555 if output_type == type and valid_mode:
2556 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2557 else:
2558 for (type, types_list, dst) in (
2559 ('SHADER', ('MIX', 'ADD'), selected_shader),
2560 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2561 ('MIX', [t[0] for t in blend_types], selected_mix),
2562 ('MATH', [t[0] for t in operations], selected_math),
2563 ('ZCOMBINE', ('MIX', ), selected_z),
2564 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2566 if merge_type == type and mode in types_list:
2567 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2568 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2569 # use only 'Mix' nodes for merging.
2570 # For that we add selected_math list to selected_mix list and clear selected_math.
2571 if selected_mix and selected_math and merge_type == 'AUTO':
2572 selected_mix += selected_math
2573 selected_math = []
2574 for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]:
2575 if not nodes_list:
2576 continue
2577 count_before = len(nodes)
2578 # sort list by loc_x - reversed
2579 nodes_list.sort(key=lambda k: k[1], reverse=True)
2580 # get maximum loc_x
2581 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2582 nodes_list.sort(key=lambda k: k[2], reverse=True)
2584 # Change the node type for math nodes in a geometry node tree.
2585 if tree_type == 'GEOMETRY':
2586 if nodes_list is selected_math or nodes_list is selected_vector:
2587 node_type = 'ShaderNode'
2588 if mode == 'MIX':
2589 mode = 'ADD'
2590 else:
2591 node_type = 'GeometryNode'
2592 if merge_position == 'CENTER':
2593 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)
2594 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2595 if do_hide:
2596 loc_y += 40
2597 else:
2598 loc_y += 80
2599 else:
2600 loc_y = nodes_list[len(nodes_list) - 1][2]
2601 offset_y = 100
2602 if not do_hide:
2603 offset_y = 200
2604 if nodes_list == selected_shader and not do_hide_shader:
2605 offset_y = 150.0
2606 the_range = len(nodes_list) - 1
2607 if len(nodes_list) == 1:
2608 the_range = 1
2609 was_multi = False
2610 for i in range(the_range):
2611 if nodes_list == selected_mix:
2612 add_type = node_type + 'MixRGB'
2613 add = nodes.new(add_type)
2614 add.blend_type = mode
2615 if mode != 'MIX':
2616 add.inputs[0].default_value = 1.0
2617 add.show_preview = False
2618 add.hide = do_hide
2619 if do_hide:
2620 loc_y = loc_y - 50
2621 first = 1
2622 second = 2
2623 add.width_hidden = 100.0
2624 elif nodes_list == selected_math:
2625 add_type = node_type + 'Math'
2626 add = nodes.new(add_type)
2627 add.operation = mode
2628 add.hide = do_hide
2629 if do_hide:
2630 loc_y = loc_y - 50
2631 first = 0
2632 second = 1
2633 add.width_hidden = 100.0
2634 elif nodes_list == selected_shader:
2635 if mode == 'MIX':
2636 add_type = node_type + 'MixShader'
2637 add = nodes.new(add_type)
2638 add.hide = do_hide_shader
2639 if do_hide_shader:
2640 loc_y = loc_y - 50
2641 first = 1
2642 second = 2
2643 add.width_hidden = 100.0
2644 elif mode == 'ADD':
2645 add_type = node_type + 'AddShader'
2646 add = nodes.new(add_type)
2647 add.hide = do_hide_shader
2648 if do_hide_shader:
2649 loc_y = loc_y - 50
2650 first = 0
2651 second = 1
2652 add.width_hidden = 100.0
2653 elif nodes_list == selected_geometry:
2654 if mode in ('JOIN', 'MIX'):
2655 add_type = node_type + 'JoinGeometry'
2656 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0])
2657 else:
2658 add_type = node_type + 'Boolean'
2659 indices = [0,1] if mode == 'DIFFERENCE' else [1]
2660 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices)
2661 add.operation = mode
2662 was_multi = True
2663 break
2664 elif nodes_list == selected_vector:
2665 add_type = node_type + 'VectorMath'
2666 add = nodes.new(add_type)
2667 add.operation = mode
2668 add.hide = do_hide
2669 if do_hide:
2670 loc_y = loc_y - 50
2671 first = 0
2672 second = 1
2673 add.width_hidden = 100.0
2674 elif nodes_list == selected_z:
2675 add = nodes.new('CompositorNodeZcombine')
2676 add.show_preview = False
2677 add.hide = do_hide
2678 if do_hide:
2679 loc_y = loc_y - 50
2680 first = 0
2681 second = 2
2682 add.width_hidden = 100.0
2683 elif nodes_list == selected_alphaover:
2684 add = nodes.new('CompositorNodeAlphaOver')
2685 add.show_preview = False
2686 add.hide = do_hide
2687 if do_hide:
2688 loc_y = loc_y - 50
2689 first = 1
2690 second = 2
2691 add.width_hidden = 100.0
2692 add.location = loc_x, loc_y
2693 loc_y += offset_y
2694 add.select = True
2696 # This has already been handled separately
2697 if was_multi:
2698 continue
2699 count_adds = i + 1
2700 count_after = len(nodes)
2701 index = count_after - 1
2702 first_selected = nodes[nodes_list[0][0]]
2703 # "last" node has been added as first, so its index is count_before.
2704 last_add = nodes[count_before]
2705 # Create list of invalid indexes.
2706 invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
2708 # Special case:
2709 # Two nodes were selected and first selected has no output links, second selected has output links.
2710 # Then add links from last add to all links 'to_socket' of out links of second selected.
2711 if len(nodes_list) == 2:
2712 if not first_selected.outputs[0].links:
2713 second_selected = nodes[nodes_list[1][0]]
2714 for ss_link in second_selected.outputs[0].links:
2715 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2716 # Link only if "to_node" index not in invalid indexes list.
2717 if not self.link_creates_cycle(ss_link, invalid_nodes):
2718 links.new(last_add.outputs[0], ss_link.to_socket)
2719 # add links from last_add to all links 'to_socket' of out links of first selected.
2720 for fs_link in first_selected.outputs[0].links:
2721 # Link only if "to_node" index not in invalid indexes list.
2722 if not self.link_creates_cycle(fs_link, invalid_nodes):
2723 links.new(last_add.outputs[0], fs_link.to_socket)
2724 # add link from "first" selected and "first" add node
2725 node_to = nodes[count_after - 1]
2726 links.new(first_selected.outputs[0], node_to.inputs[first])
2727 if node_to.type == 'ZCOMBINE':
2728 for fs_out in first_selected.outputs:
2729 if fs_out != first_selected.outputs[0] and fs_out.name in ('Z', 'Depth'):
2730 links.new(fs_out, node_to.inputs[1])
2731 break
2732 # add links between added ADD nodes and between selected and ADD nodes
2733 for i in range(count_adds):
2734 if i < count_adds - 1:
2735 node_from = nodes[index]
2736 node_to = nodes[index - 1]
2737 node_to_input_i = first
2738 node_to_z_i = 1 # if z combine - link z to first z input
2739 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2740 if node_to.type == 'ZCOMBINE':
2741 for from_out in node_from.outputs:
2742 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2743 links.new(from_out, node_to.inputs[node_to_z_i])
2744 if len(nodes_list) > 1:
2745 node_from = nodes[nodes_list[i + 1][0]]
2746 node_to = nodes[index]
2747 node_to_input_i = second
2748 node_to_z_i = 3 # if z combine - link z to second z input
2749 links.new(node_from.outputs[0], node_to.inputs[node_to_input_i])
2750 if node_to.type == 'ZCOMBINE':
2751 for from_out in node_from.outputs:
2752 if from_out != node_from.outputs[0] and from_out.name in ('Z', 'Depth'):
2753 links.new(from_out, node_to.inputs[node_to_z_i])
2754 index -= 1
2755 # set "last" of added nodes as active
2756 nodes.active = last_add
2757 for i, x, y, dx, h in nodes_list:
2758 nodes[i].select = False
2760 return {'FINISHED'}
2763 class NWBatchChangeNodes(Operator, NWBase):
2764 bl_idname = "node.nw_batch_change"
2765 bl_label = "Batch Change"
2766 bl_description = "Batch Change Blend Type and Math Operation"
2767 bl_options = {'REGISTER', 'UNDO'}
2769 blend_type: EnumProperty(
2770 name="Blend Type",
2771 items=blend_types + navs,
2773 operation: EnumProperty(
2774 name="Operation",
2775 items=operations + navs,
2778 def execute(self, context):
2779 blend_type = self.blend_type
2780 operation = self.operation
2781 for node in context.selected_nodes:
2782 if node.type == 'MIX_RGB' or node.bl_idname == 'GeometryNodeAttributeMix':
2783 if not blend_type in [nav[0] for nav in navs]:
2784 node.blend_type = blend_type
2785 else:
2786 if blend_type == 'NEXT':
2787 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2788 #index = blend_types.index(node.blend_type)
2789 if index == len(blend_types) - 1:
2790 node.blend_type = blend_types[0][0]
2791 else:
2792 node.blend_type = blend_types[index + 1][0]
2794 if blend_type == 'PREV':
2795 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2796 if index == 0:
2797 node.blend_type = blend_types[len(blend_types) - 1][0]
2798 else:
2799 node.blend_type = blend_types[index - 1][0]
2801 if node.type == 'MATH' or node.bl_idname == 'GeometryNodeAttributeMath':
2802 if not operation in [nav[0] for nav in navs]:
2803 node.operation = operation
2804 else:
2805 if operation == 'NEXT':
2806 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2807 #index = operations.index(node.operation)
2808 if index == len(operations) - 1:
2809 node.operation = operations[0][0]
2810 else:
2811 node.operation = operations[index + 1][0]
2813 if operation == 'PREV':
2814 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2815 #index = operations.index(node.operation)
2816 if index == 0:
2817 node.operation = operations[len(operations) - 1][0]
2818 else:
2819 node.operation = operations[index - 1][0]
2821 return {'FINISHED'}
2824 class NWChangeMixFactor(Operator, NWBase):
2825 bl_idname = "node.nw_factor"
2826 bl_label = "Change Factor"
2827 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2828 bl_options = {'REGISTER', 'UNDO'}
2830 # option: Change factor.
2831 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2832 # Else - change factor by option value.
2833 option: FloatProperty()
2835 def execute(self, context):
2836 nodes, links = get_nodes_links(context)
2837 option = self.option
2838 selected = [] # entry = index
2839 for si, node in enumerate(nodes):
2840 if node.select:
2841 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2842 selected.append(si)
2844 for si in selected:
2845 fac = nodes[si].inputs[0]
2846 nodes[si].hide = False
2847 if option in {0.0, 1.0}:
2848 fac.default_value = option
2849 else:
2850 fac.default_value += option
2852 return {'FINISHED'}
2855 class NWCopySettings(Operator, NWBase):
2856 bl_idname = "node.nw_copy_settings"
2857 bl_label = "Copy Settings"
2858 bl_description = "Copy Settings of Active Node to Selected Nodes"
2859 bl_options = {'REGISTER', 'UNDO'}
2861 @classmethod
2862 def poll(cls, context):
2863 valid = False
2864 if nw_check(context):
2865 if (
2866 context.active_node is not None and
2867 context.active_node.type != 'FRAME'
2869 valid = True
2870 return valid
2872 def execute(self, context):
2873 node_active = context.active_node
2874 node_selected = context.selected_nodes
2876 # Error handling
2877 if not (len(node_selected) > 1):
2878 self.report({'ERROR'}, "2 nodes must be selected at least")
2879 return {'CANCELLED'}
2881 # Check if active node is in the selection
2882 selected_node_names = [n.name for n in node_selected]
2883 if node_active.name not in selected_node_names:
2884 self.report({'ERROR'}, "No active node")
2885 return {'CANCELLED'}
2887 # Get nodes in selection by type
2888 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2890 if not (len(valid_nodes) > 1) and node_active:
2891 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2892 return {'CANCELLED'}
2894 if len(valid_nodes) != len(node_selected):
2895 # Report nodes that are not valid
2896 valid_node_names = [n.name for n in valid_nodes]
2897 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2898 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2900 # Reference original
2901 orig = node_active
2902 #node_selected_names = [n.name for n in node_selected]
2904 # Output list
2905 success_names = []
2907 # Deselect all nodes
2908 for i in node_selected:
2909 i.select = False
2911 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2912 # Run through all other nodes
2913 for node in valid_nodes[1:]:
2915 # Check for frame node
2916 parent = node.parent if node.parent else None
2917 node_loc = [node.location.x, node.location.y]
2919 # Select original to duplicate
2920 orig.select = True
2922 # Duplicate selected node
2923 bpy.ops.node.duplicate()
2924 new_node = context.selected_nodes[0]
2926 # Deselect copy
2927 new_node.select = False
2929 # Properties to copy
2930 node_tree = node.id_data
2931 props_to_copy = 'bl_idname name location height width'.split(' ')
2933 # Input and outputs
2934 reconnections = []
2935 mappings = chain.from_iterable([node.inputs, node.outputs])
2936 for i in (i for i in mappings if i.is_linked):
2937 for L in i.links:
2938 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2940 # Properties
2941 props = {j: getattr(node, j) for j in props_to_copy}
2942 props_to_copy.pop(0)
2944 for prop in props_to_copy:
2945 setattr(new_node, prop, props[prop])
2947 # Get the node tree to remove the old node
2948 nodes = node_tree.nodes
2949 nodes.remove(node)
2950 new_node.name = props['name']
2952 if parent:
2953 new_node.parent = parent
2954 new_node.location = node_loc
2956 for str_from, str_to in reconnections:
2957 node_tree.links.new(eval(str_from), eval(str_to))
2959 success_names.append(new_node.name)
2961 orig.select = True
2962 node_tree.nodes.active = orig
2963 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2964 return {'FINISHED'}
2967 class NWCopyLabel(Operator, NWBase):
2968 bl_idname = "node.nw_copy_label"
2969 bl_label = "Copy Label"
2970 bl_options = {'REGISTER', 'UNDO'}
2972 option: EnumProperty(
2973 name="option",
2974 description="Source of name of label",
2975 items=(
2976 ('FROM_ACTIVE', 'from active', 'from active node',),
2977 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2978 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2982 def execute(self, context):
2983 nodes, links = get_nodes_links(context)
2984 option = self.option
2985 active = nodes.active
2986 if option == 'FROM_ACTIVE':
2987 if active:
2988 src_label = active.label
2989 for node in [n for n in nodes if n.select and nodes.active != n]:
2990 node.label = src_label
2991 elif option == 'FROM_NODE':
2992 selected = [n for n in nodes if n.select]
2993 for node in selected:
2994 for input in node.inputs:
2995 if input.links:
2996 src = input.links[0].from_node
2997 node.label = src.label
2998 break
2999 elif option == 'FROM_SOCKET':
3000 selected = [n for n in nodes if n.select]
3001 for node in selected:
3002 for input in node.inputs:
3003 if input.links:
3004 src = input.links[0].from_socket
3005 node.label = src.name
3006 break
3008 return {'FINISHED'}
3011 class NWClearLabel(Operator, NWBase):
3012 bl_idname = "node.nw_clear_label"
3013 bl_label = "Clear Label"
3014 bl_options = {'REGISTER', 'UNDO'}
3016 option: BoolProperty()
3018 def execute(self, context):
3019 nodes, links = get_nodes_links(context)
3020 for node in [n for n in nodes if n.select]:
3021 node.label = ''
3023 return {'FINISHED'}
3025 def invoke(self, context, event):
3026 if self.option:
3027 return self.execute(context)
3028 else:
3029 return context.window_manager.invoke_confirm(self, event)
3032 class NWModifyLabels(Operator, NWBase):
3033 """Modify Labels of all selected nodes"""
3034 bl_idname = "node.nw_modify_labels"
3035 bl_label = "Modify Labels"
3036 bl_options = {'REGISTER', 'UNDO'}
3038 prepend: StringProperty(
3039 name="Add to Beginning"
3041 append: StringProperty(
3042 name="Add to End"
3044 replace_from: StringProperty(
3045 name="Text to Replace"
3047 replace_to: StringProperty(
3048 name="Replace with"
3051 def execute(self, context):
3052 nodes, links = get_nodes_links(context)
3053 for node in [n for n in nodes if n.select]:
3054 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
3056 return {'FINISHED'}
3058 def invoke(self, context, event):
3059 self.prepend = ""
3060 self.append = ""
3061 self.remove = ""
3062 return context.window_manager.invoke_props_dialog(self)
3065 class NWAddTextureSetup(Operator, NWBase):
3066 bl_idname = "node.nw_add_texture"
3067 bl_label = "Texture Setup"
3068 bl_description = "Add Texture Node Setup to Selected Shaders"
3069 bl_options = {'REGISTER', 'UNDO'}
3071 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
3073 @classmethod
3074 def poll(cls, context):
3075 valid = False
3076 if nw_check(context):
3077 space = context.space_data
3078 if space.tree_type == 'ShaderNodeTree':
3079 valid = True
3080 return valid
3082 def execute(self, context):
3083 nodes, links = get_nodes_links(context)
3084 shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
3085 texture_types = [x[1] for x in shaders_texture_nodes_props]
3086 selected_nodes = [n for n in nodes if n.select]
3087 for t_node in selected_nodes:
3088 valid = False
3089 input_index = 0
3090 if t_node.inputs:
3091 for index, i in enumerate(t_node.inputs):
3092 if not i.is_linked:
3093 valid = True
3094 input_index = index
3095 break
3096 if valid:
3097 locx = t_node.location.x
3098 locy = t_node.location.y - t_node.dimensions.y/2
3100 xoffset = [500, 700]
3101 is_texture = False
3102 if t_node.type in texture_types + ['MAPPING']:
3103 xoffset = [290, 500]
3104 is_texture = True
3106 coordout = 2
3107 image_type = 'ShaderNodeTexImage'
3109 if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
3110 coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
3111 if t_node.type == 'BACKGROUND':
3112 image_type = 'ShaderNodeTexEnvironment'
3114 if not is_texture:
3115 tex = nodes.new(image_type)
3116 tex.location = [locx - 200, locy + 112]
3117 nodes.active = tex
3118 links.new(tex.outputs[0], t_node.inputs[input_index])
3120 t_node.select = False
3121 if self.add_mapping or is_texture:
3122 if t_node.type != 'MAPPING':
3123 m = nodes.new('ShaderNodeMapping')
3124 m.location = [locx - xoffset[0], locy + 141]
3125 m.width = 240
3126 else:
3127 m = t_node
3128 coord = nodes.new('ShaderNodeTexCoord')
3129 coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
3131 if not is_texture:
3132 links.new(m.outputs[0], tex.inputs[0])
3133 links.new(coord.outputs[coordout], m.inputs[0])
3134 else:
3135 nodes.active = m
3136 links.new(m.outputs[0], t_node.inputs[input_index])
3137 links.new(coord.outputs[coordout], m.inputs[0])
3138 else:
3139 self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
3140 return {'FINISHED'}
3143 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
3144 bl_idname = "node.nw_add_textures_for_principled"
3145 bl_label = "Principled Texture Setup"
3146 bl_description = "Add Texture Node Setup for Principled BSDF"
3147 bl_options = {'REGISTER', 'UNDO'}
3149 directory: StringProperty(
3150 name='Directory',
3151 subtype='DIR_PATH',
3152 default='',
3153 description='Folder to search in for image files'
3155 files: CollectionProperty(
3156 type=bpy.types.OperatorFileListElement,
3157 options={'HIDDEN', 'SKIP_SAVE'}
3160 relative_path: BoolProperty(
3161 name='Relative Path',
3162 description='Set the file path relative to the blend file, when possible',
3163 default=True
3166 order = [
3167 "filepath",
3168 "files",
3171 def draw(self, context):
3172 layout = self.layout
3173 layout.alignment = 'LEFT'
3175 layout.prop(self, 'relative_path')
3177 @classmethod
3178 def poll(cls, context):
3179 valid = False
3180 if nw_check(context):
3181 space = context.space_data
3182 if space.tree_type == 'ShaderNodeTree':
3183 valid = True
3184 return valid
3186 def execute(self, context):
3187 # Check if everything is ok
3188 if not self.directory:
3189 self.report({'INFO'}, 'No Folder Selected')
3190 return {'CANCELLED'}
3191 if not self.files[:]:
3192 self.report({'INFO'}, 'No Files Selected')
3193 return {'CANCELLED'}
3195 nodes, links = get_nodes_links(context)
3196 active_node = nodes.active
3197 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
3198 self.report({'INFO'}, 'Select Principled BSDF')
3199 return {'CANCELLED'}
3201 # Helper_functions
3202 def split_into__components(fname):
3203 # Split filename into components
3204 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
3205 # Remove extension
3206 fname = path.splitext(fname)[0]
3207 # Remove digits
3208 fname = ''.join(i for i in fname if not i.isdigit())
3209 # Separate CamelCase by space
3210 fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",fname)
3211 # Replace common separators with SPACE
3212 separators = ['_', '.', '-', '__', '--', '#']
3213 for sep in separators:
3214 fname = fname.replace(sep, ' ')
3216 components = fname.split(' ')
3217 components = [c.lower() for c in components]
3218 return components
3220 # Filter textures names for texturetypes in filenames
3221 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
3222 tags = context.preferences.addons[__name__].preferences.principled_tags
3223 normal_abbr = tags.normal.split(' ')
3224 bump_abbr = tags.bump.split(' ')
3225 gloss_abbr = tags.gloss.split(' ')
3226 rough_abbr = tags.rough.split(' ')
3227 socketnames = [
3228 ['Displacement', tags.displacement.split(' '), None],
3229 ['Base Color', tags.base_color.split(' '), None],
3230 ['Subsurface Color', tags.sss_color.split(' '), None],
3231 ['Metallic', tags.metallic.split(' '), None],
3232 ['Specular', tags.specular.split(' '), None],
3233 ['Roughness', rough_abbr + gloss_abbr, None],
3234 ['Normal', normal_abbr + bump_abbr, None],
3235 ['Transmission', tags.transmission.split(' '), None],
3236 ['Emission', tags.emission.split(' '), None],
3237 ['Alpha', tags.alpha.split(' '), None],
3238 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
3241 # Look through texture_types and set value as filename of first matched file
3242 def match_files_to_socket_names():
3243 for sname in socketnames:
3244 for file in self.files:
3245 fname = file.name
3246 filenamecomponents = split_into__components(fname)
3247 matches = set(sname[1]).intersection(set(filenamecomponents))
3248 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
3249 if matches:
3250 sname[2] = fname
3251 break
3253 match_files_to_socket_names()
3254 # Remove socketnames without found files
3255 socketnames = [s for s in socketnames if s[2]
3256 and path.exists(self.directory+s[2])]
3257 if not socketnames:
3258 self.report({'INFO'}, 'No matching images found')
3259 print('No matching images found')
3260 return {'CANCELLED'}
3262 # Don't override path earlier as os.path is used to check the absolute path
3263 import_path = self.directory
3264 if self.relative_path:
3265 if bpy.data.filepath:
3266 try:
3267 import_path = bpy.path.relpath(self.directory)
3268 except ValueError:
3269 pass
3271 # Add found images
3272 print('\nMatched Textures:')
3273 texture_nodes = []
3274 disp_texture = None
3275 ao_texture = None
3276 normal_node = None
3277 roughness_node = None
3278 for i, sname in enumerate(socketnames):
3279 print(i, sname[0], sname[2])
3281 # DISPLACEMENT NODES
3282 if sname[0] == 'Displacement':
3283 disp_texture = nodes.new(type='ShaderNodeTexImage')
3284 img = bpy.data.images.load(path.join(import_path, sname[2]))
3285 disp_texture.image = img
3286 disp_texture.label = 'Displacement'
3287 if disp_texture.image:
3288 disp_texture.image.colorspace_settings.is_data = True
3290 # Add displacement offset nodes
3291 disp_node = nodes.new(type='ShaderNodeDisplacement')
3292 # Align the Displacement node under the active Principled BSDF node
3293 disp_node.location = active_node.location + Vector((100, -700))
3294 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
3296 # TODO Turn on true displacement in the material
3297 # Too complicated for now
3299 # Find output node
3300 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
3301 if output_node:
3302 if not output_node[0].inputs[2].is_linked:
3303 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
3305 continue
3307 # AMBIENT OCCLUSION TEXTURE
3308 if sname[0] == 'Ambient Occlusion':
3309 ao_texture = nodes.new(type='ShaderNodeTexImage')
3310 img = bpy.data.images.load(path.join(import_path, sname[2]))
3311 ao_texture.image = img
3312 ao_texture.label = sname[0]
3313 if ao_texture.image:
3314 ao_texture.image.colorspace_settings.is_data = True
3316 continue
3318 if not active_node.inputs[sname[0]].is_linked:
3319 # No texture node connected -> add texture node with new image
3320 texture_node = nodes.new(type='ShaderNodeTexImage')
3321 img = bpy.data.images.load(path.join(import_path, sname[2]))
3322 texture_node.image = img
3324 # NORMAL NODES
3325 if sname[0] == 'Normal':
3326 # Test if new texture node is normal or bump map
3327 fname_components = split_into__components(sname[2])
3328 match_normal = set(normal_abbr).intersection(set(fname_components))
3329 match_bump = set(bump_abbr).intersection(set(fname_components))
3330 if match_normal:
3331 # If Normal add normal node in between
3332 normal_node = nodes.new(type='ShaderNodeNormalMap')
3333 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
3334 elif match_bump:
3335 # If Bump add bump node in between
3336 normal_node = nodes.new(type='ShaderNodeBump')
3337 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
3339 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
3340 normal_node_texture = texture_node
3342 elif sname[0] == 'Roughness':
3343 # Test if glossy or roughness map
3344 fname_components = split_into__components(sname[2])
3345 match_rough = set(rough_abbr).intersection(set(fname_components))
3346 match_gloss = set(gloss_abbr).intersection(set(fname_components))
3348 if match_rough:
3349 # If Roughness nothing to to
3350 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
3352 elif match_gloss:
3353 # If Gloss Map add invert node
3354 invert_node = nodes.new(type='ShaderNodeInvert')
3355 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
3357 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
3358 roughness_node = texture_node
3360 else:
3361 # This is a simple connection Texture --> Input slot
3362 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
3364 # Use non-color for all but 'Base Color' Textures
3365 if not sname[0] in ['Base Color', 'Emission'] and texture_node.image:
3366 texture_node.image.colorspace_settings.is_data = True
3368 else:
3369 # If already texture connected. add to node list for alignment
3370 texture_node = active_node.inputs[sname[0]].links[0].from_node
3372 # This are all connected texture nodes
3373 texture_nodes.append(texture_node)
3374 texture_node.label = sname[0]
3376 if disp_texture:
3377 texture_nodes.append(disp_texture)
3379 if ao_texture:
3380 # We want the ambient occlusion texture to be the top most texture node
3381 texture_nodes.insert(0, ao_texture)
3383 # Alignment
3384 for i, texture_node in enumerate(texture_nodes):
3385 offset = Vector((-550, (i * -280) + 200))
3386 texture_node.location = active_node.location + offset
3388 if normal_node:
3389 # Extra alignment if normal node was added
3390 normal_node.location = normal_node_texture.location + Vector((300, 0))
3392 if roughness_node:
3393 # Alignment of invert node if glossy map
3394 invert_node.location = roughness_node.location + Vector((300, 0))
3396 # Add texture input + mapping
3397 mapping = nodes.new(type='ShaderNodeMapping')
3398 mapping.location = active_node.location + Vector((-1050, 0))
3399 if len(texture_nodes) > 1:
3400 # If more than one texture add reroute node in between
3401 reroute = nodes.new(type='NodeReroute')
3402 texture_nodes.append(reroute)
3403 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
3404 reroute.location = tex_coords + Vector((-50, -120))
3405 for texture_node in texture_nodes:
3406 link = links.new(texture_node.inputs[0], reroute.outputs[0])
3407 link = links.new(reroute.inputs[0], mapping.outputs[0])
3408 else:
3409 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
3411 # Connect texture_coordiantes to mapping node
3412 texture_input = nodes.new(type='ShaderNodeTexCoord')
3413 texture_input.location = mapping.location + Vector((-200, 0))
3414 link = links.new(mapping.inputs[0], texture_input.outputs[2])
3416 # Create frame around tex coords and mapping
3417 frame = nodes.new(type='NodeFrame')
3418 frame.label = 'Mapping'
3419 mapping.parent = frame
3420 texture_input.parent = frame
3421 frame.update()
3423 # Create frame around texture nodes
3424 frame = nodes.new(type='NodeFrame')
3425 frame.label = 'Textures'
3426 for tnode in texture_nodes:
3427 tnode.parent = frame
3428 frame.update()
3430 # Just to be sure
3431 active_node.select = False
3432 nodes.update()
3433 links.update()
3434 force_update(context)
3435 return {'FINISHED'}
3438 class NWAddReroutes(Operator, NWBase):
3439 """Add Reroute Nodes and link them to outputs of selected nodes"""
3440 bl_idname = "node.nw_add_reroutes"
3441 bl_label = "Add Reroutes"
3442 bl_description = "Add Reroutes to Outputs"
3443 bl_options = {'REGISTER', 'UNDO'}
3445 option: EnumProperty(
3446 name="option",
3447 items=[
3448 ('ALL', 'to all', 'Add to all outputs'),
3449 ('LOOSE', 'to loose', 'Add only to loose outputs'),
3450 ('LINKED', 'to linked', 'Add only to linked outputs'),
3454 def execute(self, context):
3455 tree_type = context.space_data.node_tree.type
3456 option = self.option
3457 nodes, links = get_nodes_links(context)
3458 # output valid when option is 'all' or when 'loose' output has no links
3459 valid = False
3460 post_select = [] # nodes to be selected after execution
3461 # create reroutes and recreate links
3462 for node in [n for n in nodes if n.select]:
3463 if node.outputs:
3464 x = node.location.x
3465 y = node.location.y
3466 width = node.width
3467 # unhide 'REROUTE' nodes to avoid issues with location.y
3468 if node.type == 'REROUTE':
3469 node.hide = False
3470 # When node is hidden - width_hidden not usable.
3471 # Hack needed to calculate real width
3472 if node.hide:
3473 bpy.ops.node.select_all(action='DESELECT')
3474 helper = nodes.new('NodeReroute')
3475 helper.select = True
3476 node.select = True
3477 # resize node and helper to zero. Then check locations to calculate width
3478 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
3479 width = 2.0 * (helper.location.x - node.location.x)
3480 # restore node location
3481 node.location = x, y
3482 # delete helper
3483 node.select = False
3484 # only helper is selected now
3485 bpy.ops.node.delete()
3486 x = node.location.x + width + 20.0
3487 if node.type != 'REROUTE':
3488 y -= 35.0
3489 y_offset = -22.0
3490 loc = x, y
3491 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
3492 for out_i, output in enumerate(node.outputs):
3493 pass_used = False # initial value to be analyzed if 'R_LAYERS'
3494 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3495 if node.type != 'R_LAYERS':
3496 pass_used = True
3497 else: # if 'R_LAYERS' check if output represent used render pass
3498 node_scene = node.scene
3499 node_layer = node.layer
3500 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3501 if output.name == 'Alpha':
3502 pass_used = True
3503 else:
3504 # check entries in global 'rl_outputs' variable
3505 for rlo in rl_outputs:
3506 if output.name in {rlo.output_name, rlo.exr_output_name}:
3507 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
3508 break
3509 if pass_used:
3510 valid = ((option == 'ALL') or
3511 (option == 'LOOSE' and not output.links) or
3512 (option == 'LINKED' and output.links))
3513 # Add reroutes only if valid, but offset location in all cases.
3514 if valid:
3515 n = nodes.new('NodeReroute')
3516 nodes.active = n
3517 for link in output.links:
3518 links.new(n.outputs[0], link.to_socket)
3519 links.new(output, n.inputs[0])
3520 n.location = loc
3521 post_select.append(n)
3522 reroutes_count += 1
3523 y += y_offset
3524 loc = x, y
3525 # disselect the node so that after execution of script only newly created nodes are selected
3526 node.select = False
3527 # nicer reroutes distribution along y when node.hide
3528 if node.hide:
3529 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
3530 for reroute in [r for r in nodes if r.select]:
3531 reroute.location.y -= y_translate
3532 for node in post_select:
3533 node.select = True
3535 return {'FINISHED'}
3538 class NWLinkActiveToSelected(Operator, NWBase):
3539 """Link active node to selected nodes basing on various criteria"""
3540 bl_idname = "node.nw_link_active_to_selected"
3541 bl_label = "Link Active Node to Selected"
3542 bl_options = {'REGISTER', 'UNDO'}
3544 replace: BoolProperty()
3545 use_node_name: BoolProperty()
3546 use_outputs_names: BoolProperty()
3548 @classmethod
3549 def poll(cls, context):
3550 valid = False
3551 if nw_check(context):
3552 if context.active_node is not None:
3553 if context.active_node.select:
3554 valid = True
3555 return valid
3557 def execute(self, context):
3558 nodes, links = get_nodes_links(context)
3559 replace = self.replace
3560 use_node_name = self.use_node_name
3561 use_outputs_names = self.use_outputs_names
3562 active = nodes.active
3563 selected = [node for node in nodes if node.select and node != active]
3564 outputs = [] # Only usable outputs of active nodes will be stored here.
3565 for out in active.outputs:
3566 if active.type != 'R_LAYERS':
3567 outputs.append(out)
3568 else:
3569 # 'R_LAYERS' node type needs special handling.
3570 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3571 # Only outputs that represent used passes should be taken into account
3572 # Check if pass represented by output is used.
3573 # global 'rl_outputs' list will be used for that
3574 for rlo in rl_outputs:
3575 pass_used = False # initial value. Will be set to True if pass is used
3576 if out.name == 'Alpha':
3577 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3578 pass_used = True
3579 elif out.name in {rlo.output_name, rlo.exr_output_name}:
3580 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3581 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
3582 break
3583 if pass_used:
3584 outputs.append(out)
3585 doit = True # Will be changed to False when links successfully added to previous output.
3586 for out in outputs:
3587 if doit:
3588 for node in selected:
3589 dst_name = node.name # Will be compared with src_name if needed.
3590 # When node has label - use it as dst_name
3591 if node.label:
3592 dst_name = node.label
3593 valid = True # Initial value. Will be changed to False if names don't match.
3594 src_name = dst_name # If names not used - this assignment will keep valid = True.
3595 if use_node_name:
3596 # Set src_name to source node name or label
3597 src_name = active.name
3598 if active.label:
3599 src_name = active.label
3600 elif use_outputs_names:
3601 src_name = (out.name, )
3602 for rlo in rl_outputs:
3603 if out.name in {rlo.output_name, rlo.exr_output_name}:
3604 src_name = (rlo.output_name, rlo.exr_output_name)
3605 if dst_name not in src_name:
3606 valid = False
3607 if valid:
3608 for input in node.inputs:
3609 if input.type == out.type or node.type == 'REROUTE':
3610 if replace or not input.is_linked:
3611 links.new(out, input)
3612 if not use_node_name and not use_outputs_names:
3613 doit = False
3614 break
3616 return {'FINISHED'}
3619 class NWAlignNodes(Operator, NWBase):
3620 '''Align the selected nodes neatly in a row/column'''
3621 bl_idname = "node.nw_align_nodes"
3622 bl_label = "Align Nodes"
3623 bl_options = {'REGISTER', 'UNDO'}
3624 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3626 def execute(self, context):
3627 nodes, links = get_nodes_links(context)
3628 margin = self.margin
3630 selection = []
3631 for node in nodes:
3632 if node.select and node.type != 'FRAME':
3633 selection.append(node)
3635 # If no nodes are selected, align all nodes
3636 active_loc = None
3637 if not selection:
3638 selection = nodes
3639 elif nodes.active in selection:
3640 active_loc = copy(nodes.active.location) # make a copy, not a reference
3642 # Check if nodes should be laid out horizontally or vertically
3643 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3644 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3645 x_range = max(x_locs) - min(x_locs)
3646 y_range = max(y_locs) - min(y_locs)
3647 mid_x = (max(x_locs) + min(x_locs)) / 2
3648 mid_y = (max(y_locs) + min(y_locs)) / 2
3649 horizontal = x_range > y_range
3651 # Sort selection by location of node mid-point
3652 if horizontal:
3653 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3654 else:
3655 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3657 # Alignment
3658 current_pos = 0
3659 for node in selection:
3660 current_margin = margin
3661 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3663 if horizontal:
3664 node.location.x = current_pos
3665 current_pos += current_margin + node.dimensions.x
3666 node.location.y = mid_y + (node.dimensions.y / 2)
3667 else:
3668 node.location.y = current_pos
3669 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3670 node.location.x = mid_x - (node.dimensions.x / 2)
3672 # If active node is selected, center nodes around it
3673 if active_loc is not None:
3674 active_loc_diff = active_loc - nodes.active.location
3675 for node in selection:
3676 node.location += active_loc_diff
3677 else: # Position nodes centered around where they used to be
3678 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])
3679 new_mid = (max(locs) + min(locs)) / 2
3680 for node in selection:
3681 if horizontal:
3682 node.location.x += (mid_x - new_mid)
3683 else:
3684 node.location.y += (mid_y - new_mid)
3686 return {'FINISHED'}
3689 class NWSelectParentChildren(Operator, NWBase):
3690 bl_idname = "node.nw_select_parent_child"
3691 bl_label = "Select Parent or Children"
3692 bl_options = {'REGISTER', 'UNDO'}
3694 option: EnumProperty(
3695 name="option",
3696 items=(
3697 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3698 ('CHILD', 'Select Children', 'Select members of selected frame'),
3702 def execute(self, context):
3703 nodes, links = get_nodes_links(context)
3704 option = self.option
3705 selected = [node for node in nodes if node.select]
3706 if option == 'PARENT':
3707 for sel in selected:
3708 parent = sel.parent
3709 if parent:
3710 parent.select = True
3711 else: # option == 'CHILD'
3712 for sel in selected:
3713 children = [node for node in nodes if node.parent == sel]
3714 for kid in children:
3715 kid.select = True
3717 return {'FINISHED'}
3720 class NWDetachOutputs(Operator, NWBase):
3721 """Detach outputs of selected node leaving inputs linked"""
3722 bl_idname = "node.nw_detach_outputs"
3723 bl_label = "Detach Outputs"
3724 bl_options = {'REGISTER', 'UNDO'}
3726 def execute(self, context):
3727 nodes, links = get_nodes_links(context)
3728 selected = context.selected_nodes
3729 bpy.ops.node.duplicate_move_keep_inputs()
3730 new_nodes = context.selected_nodes
3731 bpy.ops.node.select_all(action="DESELECT")
3732 for node in selected:
3733 node.select = True
3734 bpy.ops.node.delete_reconnect()
3735 for new_node in new_nodes:
3736 new_node.select = True
3737 bpy.ops.transform.translate('INVOKE_DEFAULT')
3739 return {'FINISHED'}
3742 class NWLinkToOutputNode(Operator):
3743 """Link to Composite node or Material Output node"""
3744 bl_idname = "node.nw_link_out"
3745 bl_label = "Connect to Output"
3746 bl_options = {'REGISTER', 'UNDO'}
3748 @classmethod
3749 def poll(cls, context):
3750 valid = False
3751 if nw_check(context):
3752 if context.active_node is not None:
3753 for out in context.active_node.outputs:
3754 if is_visible_socket(out):
3755 valid = True
3756 break
3757 return valid
3759 def execute(self, context):
3760 nodes, links = get_nodes_links(context)
3761 active = nodes.active
3762 output_node = None
3763 output_index = None
3764 tree_type = context.space_data.tree_type
3765 if tree_type == 'ShaderNodeTree':
3766 output_types = [x[1] for x in shaders_output_nodes_props] + ['OUTPUT']
3767 elif tree_type == 'CompositorNodeTree':
3768 output_types = ['COMPOSITE']
3769 elif tree_type == 'TextureNodeTree':
3770 output_types = ['OUTPUT']
3771 elif tree_type == 'GeometryNodeTree':
3772 output_types = ['GROUP_OUTPUT']
3773 for node in nodes:
3774 if node.type in output_types:
3775 output_node = node
3776 break
3777 if not output_node:
3778 bpy.ops.node.select_all(action="DESELECT")
3779 if tree_type == 'ShaderNodeTree':
3780 if context.space_data.shader_type == 'OBJECT':
3781 output_node = nodes.new('ShaderNodeOutputMaterial')
3782 elif context.space_data.shader_type == 'WORLD':
3783 output_node = nodes.new('ShaderNodeOutputWorld')
3784 elif tree_type == 'CompositorNodeTree':
3785 output_node = nodes.new('CompositorNodeComposite')
3786 elif tree_type == 'TextureNodeTree':
3787 output_node = nodes.new('TextureNodeOutput')
3788 elif tree_type == 'GeometryNodeTree':
3789 output_node = nodes.new('NodeGroupOutput')
3790 output_node.location.x = active.location.x + active.dimensions.x + 80
3791 output_node.location.y = active.location.y
3792 if (output_node and active.outputs):
3793 for i, output in enumerate(active.outputs):
3794 if is_visible_socket(output):
3795 output_index = i
3796 break
3797 for i, output in enumerate(active.outputs):
3798 if output.type == output_node.inputs[0].type and is_visible_socket(output):
3799 output_index = i
3800 break
3802 out_input_index = 0
3803 if tree_type == 'ShaderNodeTree':
3804 if active.outputs[output_index].name == 'Volume':
3805 out_input_index = 1
3806 elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
3807 out_input_index = 2
3808 elif tree_type == 'GeometryNodeTree':
3809 if active.outputs[output_index].type != 'GEOMETRY':
3810 return {'CANCELLED'}
3811 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3813 force_update(context) # viewport render does not update
3815 return {'FINISHED'}
3818 class NWMakeLink(Operator, NWBase):
3819 """Make a link from one socket to another"""
3820 bl_idname = 'node.nw_make_link'
3821 bl_label = 'Make Link'
3822 bl_options = {'REGISTER', 'UNDO'}
3823 from_socket: IntProperty()
3824 to_socket: IntProperty()
3826 def execute(self, context):
3827 nodes, links = get_nodes_links(context)
3829 n1 = nodes[context.scene.NWLazySource]
3830 n2 = nodes[context.scene.NWLazyTarget]
3832 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3834 force_update(context)
3836 return {'FINISHED'}
3839 class NWCallInputsMenu(Operator, NWBase):
3840 """Link from this output"""
3841 bl_idname = 'node.nw_call_inputs_menu'
3842 bl_label = 'Make Link'
3843 bl_options = {'REGISTER', 'UNDO'}
3844 from_socket: IntProperty()
3846 def execute(self, context):
3847 nodes, links = get_nodes_links(context)
3849 context.scene.NWSourceSocket = self.from_socket
3851 n1 = nodes[context.scene.NWLazySource]
3852 n2 = nodes[context.scene.NWLazyTarget]
3853 if len(n2.inputs) > 1:
3854 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3855 elif len(n2.inputs) == 1:
3856 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3857 return {'FINISHED'}
3860 class NWAddSequence(Operator, NWBase, ImportHelper):
3861 """Add an Image Sequence"""
3862 bl_idname = 'node.nw_add_sequence'
3863 bl_label = 'Import Image Sequence'
3864 bl_options = {'REGISTER', 'UNDO'}
3866 directory: StringProperty(
3867 subtype="DIR_PATH"
3869 filename: StringProperty(
3870 subtype="FILE_NAME"
3872 files: CollectionProperty(
3873 type=bpy.types.OperatorFileListElement,
3874 options={'HIDDEN', 'SKIP_SAVE'}
3876 relative_path: BoolProperty(
3877 name='Relative Path',
3878 description='Set the file path relative to the blend file, when possible',
3879 default=True
3882 def draw(self, context):
3883 layout = self.layout
3884 layout.alignment = 'LEFT'
3886 layout.prop(self, 'relative_path')
3888 def execute(self, context):
3889 nodes, links = get_nodes_links(context)
3890 directory = self.directory
3891 filename = self.filename
3892 files = self.files
3893 tree = context.space_data.node_tree
3895 # DEBUG
3896 # print ("\nDIR:", directory)
3897 # print ("FN:", filename)
3898 # print ("Fs:", list(f.name for f in files), '\n')
3900 if tree.type == 'SHADER':
3901 node_type = "ShaderNodeTexImage"
3902 elif tree.type == 'COMPOSITING':
3903 node_type = "CompositorNodeImage"
3904 else:
3905 self.report({'ERROR'}, "Unsupported Node Tree type!")
3906 return {'CANCELLED'}
3908 if not files[0].name and not filename:
3909 self.report({'ERROR'}, "No file chosen")
3910 return {'CANCELLED'}
3911 elif files[0].name and (not filename or not path.exists(directory+filename)):
3912 # User has selected multiple files without an active one, or the active one is non-existant
3913 filename = files[0].name
3915 if not path.exists(directory+filename):
3916 self.report({'ERROR'}, filename+" does not exist!")
3917 return {'CANCELLED'}
3919 without_ext = '.'.join(filename.split('.')[:-1])
3921 # if last digit isn't a number, it's not a sequence
3922 if not without_ext[-1].isdigit():
3923 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3924 return {'CANCELLED'}
3927 extension = filename.split('.')[-1]
3928 reverse = without_ext[::-1] # reverse string
3930 count_numbers = 0
3931 for char in reverse:
3932 if char.isdigit():
3933 count_numbers += 1
3934 else:
3935 break
3937 without_num = without_ext[:count_numbers*-1]
3939 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3941 num_frames = len(files)
3943 nodes_list = [node for node in nodes]
3944 if nodes_list:
3945 nodes_list.sort(key=lambda k: k.location.x)
3946 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3947 yloc = 0
3948 for node in nodes:
3949 node.select = False
3950 yloc += node_mid_pt(node, 'y')
3951 yloc = yloc/len(nodes)
3952 else:
3953 xloc = 0
3954 yloc = 0
3956 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3958 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3959 node = nodes.active
3960 node.label = name_with_hashes
3962 filepath = directory+(without_ext+'.'+extension)
3963 if self.relative_path:
3964 if bpy.data.filepath:
3965 try:
3966 filepath = bpy.path.relpath(filepath)
3967 except ValueError:
3968 pass
3970 img = bpy.data.images.load(filepath)
3971 img.source = 'SEQUENCE'
3972 img.name = name_with_hashes
3973 node.image = img
3974 image_user = node.image_user if tree.type == 'SHADER' else node
3975 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
3976 image_user.frame_duration = num_frames
3978 return {'FINISHED'}
3981 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
3982 """Add multiple images at once"""
3983 bl_idname = 'node.nw_add_multiple_images'
3984 bl_label = 'Open Selected Images'
3985 bl_options = {'REGISTER', 'UNDO'}
3986 directory: StringProperty(
3987 subtype="DIR_PATH"
3989 files: CollectionProperty(
3990 type=bpy.types.OperatorFileListElement,
3991 options={'HIDDEN', 'SKIP_SAVE'}
3994 def execute(self, context):
3995 nodes, links = get_nodes_links(context)
3997 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3999 if context.space_data.node_tree.type == 'SHADER':
4000 node_type = "ShaderNodeTexImage"
4001 elif context.space_data.node_tree.type == 'COMPOSITING':
4002 node_type = "CompositorNodeImage"
4003 else:
4004 self.report({'ERROR'}, "Unsupported Node Tree type!")
4005 return {'CANCELLED'}
4007 new_nodes = []
4008 for f in self.files:
4009 fname = f.name
4011 node = nodes.new(node_type)
4012 new_nodes.append(node)
4013 node.label = fname
4014 node.hide = True
4015 node.width_hidden = 100
4016 node.location.x = xloc
4017 node.location.y = yloc
4018 yloc -= 40
4020 img = bpy.data.images.load(self.directory+fname)
4021 node.image = img
4023 # shift new nodes up to center of tree
4024 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
4025 for node in nodes:
4026 if node in new_nodes:
4027 node.select = True
4028 node.location.y += (list_size/2)
4029 else:
4030 node.select = False
4031 return {'FINISHED'}
4034 class NWViewerFocus(bpy.types.Operator):
4035 """Set the viewer tile center to the mouse position"""
4036 bl_idname = "node.nw_viewer_focus"
4037 bl_label = "Viewer Focus"
4039 x: bpy.props.IntProperty()
4040 y: bpy.props.IntProperty()
4042 @classmethod
4043 def poll(cls, context):
4044 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
4046 def execute(self, context):
4047 return {'FINISHED'}
4049 def invoke(self, context, event):
4050 render = context.scene.render
4051 space = context.space_data
4052 percent = render.resolution_percentage*0.01
4054 nodes, links = get_nodes_links(context)
4055 viewers = [n for n in nodes if n.type == 'VIEWER']
4057 if viewers:
4058 mlocx = event.mouse_region_x
4059 mlocy = event.mouse_region_y
4060 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
4062 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
4063 region_x = context.region.width
4064 region_y = context.region.height
4066 region_center_x = context.region.width / 2
4067 region_center_y = context.region.height / 2
4069 bd_x = render.resolution_x * percent * space.backdrop_zoom
4070 bd_y = render.resolution_y * percent * space.backdrop_zoom
4072 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
4073 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
4075 margin_x = region_center_x - backdrop_center_x
4076 margin_y = region_center_y - backdrop_center_y
4078 abs_mouse_x = (mlocx - margin_x) / bd_x
4079 abs_mouse_y = (mlocy - margin_y) / bd_y
4081 for node in viewers:
4082 node.center_x = abs_mouse_x
4083 node.center_y = abs_mouse_y
4084 else:
4085 return {'PASS_THROUGH'}
4087 return self.execute(context)
4090 class NWSaveViewer(bpy.types.Operator, ExportHelper):
4091 """Save the current viewer node to an image file"""
4092 bl_idname = "node.nw_save_viewer"
4093 bl_label = "Save This Image"
4094 filepath: StringProperty(subtype="FILE_PATH")
4095 filename_ext: EnumProperty(
4096 name="Format",
4097 description="Choose the file format to save to",
4098 items=(('.bmp', "BMP", ""),
4099 ('.rgb', 'IRIS', ""),
4100 ('.png', 'PNG', ""),
4101 ('.jpg', 'JPEG', ""),
4102 ('.jp2', 'JPEG2000', ""),
4103 ('.tga', 'TARGA', ""),
4104 ('.cin', 'CINEON', ""),
4105 ('.dpx', 'DPX', ""),
4106 ('.exr', 'OPEN_EXR', ""),
4107 ('.hdr', 'HDR', ""),
4108 ('.tif', 'TIFF', "")),
4109 default='.png',
4112 @classmethod
4113 def poll(cls, context):
4114 valid = False
4115 if nw_check(context):
4116 if context.space_data.tree_type == 'CompositorNodeTree':
4117 if "Viewer Node" in [i.name for i in bpy.data.images]:
4118 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
4119 valid = True
4120 return valid
4122 def execute(self, context):
4123 fp = self.filepath
4124 if fp:
4125 formats = {
4126 '.bmp': 'BMP',
4127 '.rgb': 'IRIS',
4128 '.png': 'PNG',
4129 '.jpg': 'JPEG',
4130 '.jpeg': 'JPEG',
4131 '.jp2': 'JPEG2000',
4132 '.tga': 'TARGA',
4133 '.cin': 'CINEON',
4134 '.dpx': 'DPX',
4135 '.exr': 'OPEN_EXR',
4136 '.hdr': 'HDR',
4137 '.tiff': 'TIFF',
4138 '.tif': 'TIFF'}
4139 basename, ext = path.splitext(fp)
4140 old_render_format = context.scene.render.image_settings.file_format
4141 context.scene.render.image_settings.file_format = formats[self.filename_ext]
4142 context.area.type = "IMAGE_EDITOR"
4143 context.area.spaces[0].image = bpy.data.images['Viewer Node']
4144 context.area.spaces[0].image.save_render(fp)
4145 context.area.type = "NODE_EDITOR"
4146 context.scene.render.image_settings.file_format = old_render_format
4147 return {'FINISHED'}
4150 class NWResetNodes(bpy.types.Operator):
4151 """Reset Nodes in Selection"""
4152 bl_idname = "node.nw_reset_nodes"
4153 bl_label = "Reset Nodes"
4154 bl_options = {'REGISTER', 'UNDO'}
4156 @classmethod
4157 def poll(cls, context):
4158 space = context.space_data
4159 return space.type == 'NODE_EDITOR'
4161 def execute(self, context):
4162 node_active = context.active_node
4163 node_selected = context.selected_nodes
4164 node_ignore = ["FRAME","REROUTE", "GROUP"]
4166 # Check if one node is selected at least
4167 if not (len(node_selected) > 0):
4168 self.report({'ERROR'}, "1 node must be selected at least")
4169 return {'CANCELLED'}
4171 active_node_name = node_active.name if node_active.select else None
4172 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
4174 # Create output lists
4175 selected_node_names = [n.name for n in node_selected]
4176 success_names = []
4178 # Reset all valid children in a frame
4179 node_active_is_frame = False
4180 if len(node_selected) == 1 and node_active.type == "FRAME":
4181 node_tree = node_active.id_data
4182 children = [n for n in node_tree.nodes if n.parent == node_active]
4183 if children:
4184 valid_nodes = [n for n in children if n.type not in node_ignore]
4185 selected_node_names = [n.name for n in children if n.type not in node_ignore]
4186 node_active_is_frame = True
4188 # Check if valid nodes in selection
4189 if not (len(valid_nodes) > 0):
4190 # Check for frames only
4191 frames_selected = [n for n in node_selected if n.type == "FRAME"]
4192 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
4193 self.report({'ERROR'}, "Please select only 1 frame to reset")
4194 else:
4195 self.report({'ERROR'}, "No valid node(s) in selection")
4196 return {'CANCELLED'}
4198 # Report nodes that are not valid
4199 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
4200 valid_node_names = [n.name for n in valid_nodes]
4201 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
4202 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
4204 # Deselect all nodes
4205 for i in node_selected:
4206 i.select = False
4208 # Run through all valid nodes
4209 for node in valid_nodes:
4211 parent = node.parent if node.parent else None
4212 node_loc = [node.location.x, node.location.y]
4214 node_tree = node.id_data
4215 props_to_copy = 'bl_idname name location height width'.split(' ')
4217 reconnections = []
4218 mappings = chain.from_iterable([node.inputs, node.outputs])
4219 for i in (i for i in mappings if i.is_linked):
4220 for L in i.links:
4221 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
4223 props = {j: getattr(node, j) for j in props_to_copy}
4225 new_node = node_tree.nodes.new(props['bl_idname'])
4226 props_to_copy.pop(0)
4228 for prop in props_to_copy:
4229 setattr(new_node, prop, props[prop])
4231 nodes = node_tree.nodes
4232 nodes.remove(node)
4233 new_node.name = props['name']
4235 if parent:
4236 new_node.parent = parent
4237 new_node.location = node_loc
4239 for str_from, str_to in reconnections:
4240 node_tree.links.new(eval(str_from), eval(str_to))
4242 new_node.select = False
4243 success_names.append(new_node.name)
4245 # Reselect all nodes
4246 if selected_node_names and node_active_is_frame is False:
4247 for i in selected_node_names:
4248 node_tree.nodes[i].select = True
4250 if active_node_name is not None:
4251 node_tree.nodes[active_node_name].select = True
4252 node_tree.nodes.active = node_tree.nodes[active_node_name]
4254 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
4255 return {'FINISHED'}
4259 # P A N E L
4262 def drawlayout(context, layout, mode='non-panel'):
4263 tree_type = context.space_data.tree_type
4265 col = layout.column(align=True)
4266 col.menu(NWMergeNodesMenu.bl_idname)
4267 col.separator()
4269 col = layout.column(align=True)
4270 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
4271 col.separator()
4273 if tree_type == 'ShaderNodeTree':
4274 col = layout.column(align=True)
4275 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
4276 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
4277 col.separator()
4279 col = layout.column(align=True)
4280 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
4281 col.operator(NWSwapLinks.bl_idname)
4282 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
4283 col.separator()
4285 col = layout.column(align=True)
4286 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
4287 if tree_type != 'GeometryNodeTree':
4288 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
4289 col.separator()
4291 col = layout.column(align=True)
4292 if mode == 'panel':
4293 row = col.row(align=True)
4294 row.operator(NWClearLabel.bl_idname).option = True
4295 row.operator(NWModifyLabels.bl_idname)
4296 else:
4297 col.operator(NWClearLabel.bl_idname).option = True
4298 col.operator(NWModifyLabels.bl_idname)
4299 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
4300 col.separator()
4301 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
4302 col.separator()
4304 col = layout.column(align=True)
4305 if tree_type == 'CompositorNodeTree':
4306 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
4307 if tree_type != 'GeometryNodeTree':
4308 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
4309 col.separator()
4311 col = layout.column(align=True)
4312 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
4313 col.separator()
4315 col = layout.column(align=True)
4316 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
4317 col.separator()
4319 col = layout.column(align=True)
4320 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
4321 col.separator()
4324 class NodeWranglerPanel(Panel, NWBase):
4325 bl_idname = "NODE_PT_nw_node_wrangler"
4326 bl_space_type = 'NODE_EDITOR'
4327 bl_label = "Node Wrangler"
4328 bl_region_type = "UI"
4329 bl_category = "Node Wrangler"
4331 prepend: StringProperty(
4332 name='prepend',
4334 append: StringProperty()
4335 remove: StringProperty()
4337 def draw(self, context):
4338 self.layout.label(text="(Quick access: Shift+W)")
4339 drawlayout(context, self.layout, mode='panel')
4343 # M E N U S
4345 class NodeWranglerMenu(Menu, NWBase):
4346 bl_idname = "NODE_MT_nw_node_wrangler_menu"
4347 bl_label = "Node Wrangler"
4349 def draw(self, context):
4350 self.layout.operator_context = 'INVOKE_DEFAULT'
4351 drawlayout(context, self.layout)
4354 class NWMergeNodesMenu(Menu, NWBase):
4355 bl_idname = "NODE_MT_nw_merge_nodes_menu"
4356 bl_label = "Merge Selected Nodes"
4358 def draw(self, context):
4359 type = context.space_data.tree_type
4360 layout = self.layout
4361 if type == 'ShaderNodeTree':
4362 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
4363 if type == 'GeometryNodeTree':
4364 layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes")
4365 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
4366 else:
4367 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
4368 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
4369 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
4370 props.mode = 'MIX'
4371 props.merge_type = 'ZCOMBINE'
4372 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
4373 props.mode = 'MIX'
4374 props.merge_type = 'ALPHAOVER'
4376 class NWMergeGeometryMenu(Menu, NWBase):
4377 bl_idname = "NODE_MT_nw_merge_geometry_menu"
4378 bl_label = "Merge Selected Nodes using Geometry Nodes"
4379 def draw(self, context):
4380 layout = self.layout
4381 # The boolean node + Join Geometry node
4382 for type, name, description in geo_combine_operations:
4383 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4384 props.mode = type
4385 props.merge_type = 'GEOMETRY'
4387 class NWMergeShadersMenu(Menu, NWBase):
4388 bl_idname = "NODE_MT_nw_merge_shaders_menu"
4389 bl_label = "Merge Selected Nodes using Shaders"
4391 def draw(self, context):
4392 layout = self.layout
4393 for type in ('MIX', 'ADD'):
4394 props = layout.operator(NWMergeNodes.bl_idname, text=type)
4395 props.mode = type
4396 props.merge_type = 'SHADER'
4399 class NWMergeMixMenu(Menu, NWBase):
4400 bl_idname = "NODE_MT_nw_merge_mix_menu"
4401 bl_label = "Merge Selected Nodes using Mix"
4403 def draw(self, context):
4404 layout = self.layout
4405 for type, name, description in blend_types:
4406 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4407 props.mode = type
4408 props.merge_type = 'MIX'
4411 class NWConnectionListOutputs(Menu, NWBase):
4412 bl_idname = "NODE_MT_nw_connection_list_out"
4413 bl_label = "From:"
4415 def draw(self, context):
4416 layout = self.layout
4417 nodes, links = get_nodes_links(context)
4419 n1 = nodes[context.scene.NWLazySource]
4420 index=0
4421 for o in n1.outputs:
4422 # Only show sockets that are exposed.
4423 if o.enabled:
4424 layout.operator(NWCallInputsMenu.bl_idname, text=o.name, icon="RADIOBUT_OFF").from_socket=index
4425 index+=1
4428 class NWConnectionListInputs(Menu, NWBase):
4429 bl_idname = "NODE_MT_nw_connection_list_in"
4430 bl_label = "To:"
4432 def draw(self, context):
4433 layout = self.layout
4434 nodes, links = get_nodes_links(context)
4436 n2 = nodes[context.scene.NWLazyTarget]
4438 index = 0
4439 for i in n2.inputs:
4440 # Only show sockets that are exposed.
4441 # This prevents, for example, the scale value socket
4442 # of the vector math node being added to the list when
4443 # the mode is not 'SCALE'.
4444 if i.enabled:
4445 op = layout.operator(NWMakeLink.bl_idname, text=i.name, icon="FORWARD")
4446 op.from_socket = context.scene.NWSourceSocket
4447 op.to_socket = index
4448 index+=1
4451 class NWMergeMathMenu(Menu, NWBase):
4452 bl_idname = "NODE_MT_nw_merge_math_menu"
4453 bl_label = "Merge Selected Nodes using Math"
4455 def draw(self, context):
4456 layout = self.layout
4457 for type, name, description in operations:
4458 props = layout.operator(NWMergeNodes.bl_idname, text=name)
4459 props.mode = type
4460 props.merge_type = 'MATH'
4463 class NWBatchChangeNodesMenu(Menu, NWBase):
4464 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
4465 bl_label = "Batch Change Selected Nodes"
4467 def draw(self, context):
4468 layout = self.layout
4469 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
4470 layout.menu(NWBatchChangeOperationMenu.bl_idname)
4473 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
4474 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
4475 bl_label = "Batch Change Blend Type"
4477 def draw(self, context):
4478 layout = self.layout
4479 for type, name, description in blend_types:
4480 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
4481 props.blend_type = type
4482 props.operation = 'CURRENT'
4485 class NWBatchChangeOperationMenu(Menu, NWBase):
4486 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
4487 bl_label = "Batch Change Math Operation"
4489 def draw(self, context):
4490 layout = self.layout
4491 for type, name, description in operations:
4492 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
4493 props.blend_type = 'CURRENT'
4494 props.operation = type
4497 class NWCopyToSelectedMenu(Menu, NWBase):
4498 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
4499 bl_label = "Copy to Selected"
4501 def draw(self, context):
4502 layout = self.layout
4503 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
4504 layout.menu(NWCopyLabelMenu.bl_idname)
4507 class NWCopyLabelMenu(Menu, NWBase):
4508 bl_idname = "NODE_MT_nw_copy_label_menu"
4509 bl_label = "Copy Label"
4511 def draw(self, context):
4512 layout = self.layout
4513 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
4514 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
4515 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
4518 class NWAddReroutesMenu(Menu, NWBase):
4519 bl_idname = "NODE_MT_nw_add_reroutes_menu"
4520 bl_label = "Add Reroutes"
4521 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
4523 def draw(self, context):
4524 layout = self.layout
4525 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
4526 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
4527 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
4530 class NWLinkActiveToSelectedMenu(Menu, NWBase):
4531 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
4532 bl_label = "Link Active to Selected"
4534 def draw(self, context):
4535 layout = self.layout
4536 layout.menu(NWLinkStandardMenu.bl_idname)
4537 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
4538 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
4541 class NWLinkStandardMenu(Menu, NWBase):
4542 bl_idname = "NODE_MT_nw_link_standard_menu"
4543 bl_label = "To All Selected"
4545 def draw(self, context):
4546 layout = self.layout
4547 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4548 props.replace = False
4549 props.use_node_name = False
4550 props.use_outputs_names = False
4551 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4552 props.replace = True
4553 props.use_node_name = False
4554 props.use_outputs_names = False
4557 class NWLinkUseNodeNameMenu(Menu, NWBase):
4558 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
4559 bl_label = "Use Node Name/Label"
4561 def draw(self, context):
4562 layout = self.layout
4563 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4564 props.replace = False
4565 props.use_node_name = True
4566 props.use_outputs_names = False
4567 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4568 props.replace = True
4569 props.use_node_name = True
4570 props.use_outputs_names = False
4573 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
4574 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
4575 bl_label = "Use Outputs Names"
4577 def draw(self, context):
4578 layout = self.layout
4579 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4580 props.replace = False
4581 props.use_node_name = False
4582 props.use_outputs_names = True
4583 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4584 props.replace = True
4585 props.use_node_name = False
4586 props.use_outputs_names = True
4589 class NWAttributeMenu(bpy.types.Menu):
4590 bl_idname = "NODE_MT_nw_node_attribute_menu"
4591 bl_label = "Attributes"
4593 @classmethod
4594 def poll(cls, context):
4595 valid = False
4596 if nw_check(context):
4597 snode = context.space_data
4598 valid = snode.tree_type == 'ShaderNodeTree'
4599 return valid
4601 def draw(self, context):
4602 l = self.layout
4603 nodes, links = get_nodes_links(context)
4604 mat = context.object.active_material
4606 objs = []
4607 for obj in bpy.data.objects:
4608 for slot in obj.material_slots:
4609 if slot.material == mat:
4610 objs.append(obj)
4611 attrs = []
4612 for obj in objs:
4613 if obj.data.attributes:
4614 for attr in obj.data.attributes:
4615 attrs.append(attr.name)
4616 attrs = list(set(attrs)) # get a unique list
4618 if attrs:
4619 for attr in attrs:
4620 l.operator(NWAddAttrNode.bl_idname, text=attr).attr_name = attr
4621 else:
4622 l.label(text="No attributes on objects with this material")
4625 class NWSwitchNodeTypeMenu(Menu, NWBase):
4626 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4627 bl_label = "Switch Type to..."
4629 def draw(self, context):
4630 layout = self.layout
4631 tree = context.space_data.node_tree
4632 if tree.type == 'SHADER':
4633 layout.menu(NWSwitchShadersInputSubmenu.bl_idname)
4634 layout.menu(NWSwitchShadersOutputSubmenu.bl_idname)
4635 layout.menu(NWSwitchShadersShaderSubmenu.bl_idname)
4636 layout.menu(NWSwitchShadersTextureSubmenu.bl_idname)
4637 layout.menu(NWSwitchShadersColorSubmenu.bl_idname)
4638 layout.menu(NWSwitchShadersVectorSubmenu.bl_idname)
4639 layout.menu(NWSwitchShadersConverterSubmenu.bl_idname)
4640 layout.menu(NWSwitchShadersLayoutSubmenu.bl_idname)
4641 if tree.type == 'COMPOSITING':
4642 layout.menu(NWSwitchCompoInputSubmenu.bl_idname)
4643 layout.menu(NWSwitchCompoOutputSubmenu.bl_idname)
4644 layout.menu(NWSwitchCompoColorSubmenu.bl_idname)
4645 layout.menu(NWSwitchCompoConverterSubmenu.bl_idname)
4646 layout.menu(NWSwitchCompoFilterSubmenu.bl_idname)
4647 layout.menu(NWSwitchCompoVectorSubmenu.bl_idname)
4648 layout.menu(NWSwitchCompoMatteSubmenu.bl_idname)
4649 layout.menu(NWSwitchCompoDistortSubmenu.bl_idname)
4650 layout.menu(NWSwitchCompoLayoutSubmenu.bl_idname)
4651 if tree.type == 'TEXTURE':
4652 layout.menu(NWSwitchTexInputSubmenu.bl_idname)
4653 layout.menu(NWSwitchTexOutputSubmenu.bl_idname)
4654 layout.menu(NWSwitchTexColorSubmenu.bl_idname)
4655 layout.menu(NWSwitchTexPatternSubmenu.bl_idname)
4656 layout.menu(NWSwitchTexTexturesSubmenu.bl_idname)
4657 layout.menu(NWSwitchTexConverterSubmenu.bl_idname)
4658 layout.menu(NWSwitchTexDistortSubmenu.bl_idname)
4659 layout.menu(NWSwitchTexLayoutSubmenu.bl_idname)
4660 if tree.type == 'GEOMETRY':
4661 categories = [c for c in node_categories_iter(context)
4662 if c.name not in ['Group', 'Script']]
4663 for cat in categories:
4664 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4665 if hasattr(bpy.types, idname):
4666 layout.menu(idname)
4667 else:
4668 layout.label(text="Unable to load altered node lists.")
4669 layout.label(text="Please re-enable Node Wrangler.")
4670 break
4673 class NWSwitchShadersInputSubmenu(Menu, NWBase):
4674 bl_idname = "NODE_MT_nw_switch_shaders_input_submenu"
4675 bl_label = "Input"
4677 def draw(self, context):
4678 layout = self.layout
4679 for ident, node_type, rna_name in shaders_input_nodes_props:
4680 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4681 props.to_type = ident
4684 class NWSwitchShadersOutputSubmenu(Menu, NWBase):
4685 bl_idname = "NODE_MT_nw_switch_shaders_output_submenu"
4686 bl_label = "Output"
4688 def draw(self, context):
4689 layout = self.layout
4690 for ident, node_type, rna_name in shaders_output_nodes_props:
4691 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4692 props.to_type = ident
4695 class NWSwitchShadersShaderSubmenu(Menu, NWBase):
4696 bl_idname = "NODE_MT_nw_switch_shaders_shader_submenu"
4697 bl_label = "Shader"
4699 def draw(self, context):
4700 layout = self.layout
4701 for ident, node_type, rna_name in shaders_shader_nodes_props:
4702 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4703 props.to_type = ident
4706 class NWSwitchShadersTextureSubmenu(Menu, NWBase):
4707 bl_idname = "NODE_MT_nw_switch_shaders_texture_submenu"
4708 bl_label = "Texture"
4710 def draw(self, context):
4711 layout = self.layout
4712 for ident, node_type, rna_name in shaders_texture_nodes_props:
4713 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4714 props.to_type = ident
4717 class NWSwitchShadersColorSubmenu(Menu, NWBase):
4718 bl_idname = "NODE_MT_nw_switch_shaders_color_submenu"
4719 bl_label = "Color"
4721 def draw(self, context):
4722 layout = self.layout
4723 for ident, node_type, rna_name in shaders_color_nodes_props:
4724 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4725 props.to_type = ident
4728 class NWSwitchShadersVectorSubmenu(Menu, NWBase):
4729 bl_idname = "NODE_MT_nw_switch_shaders_vector_submenu"
4730 bl_label = "Vector"
4732 def draw(self, context):
4733 layout = self.layout
4734 for ident, node_type, rna_name in shaders_vector_nodes_props:
4735 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4736 props.to_type = ident
4739 class NWSwitchShadersConverterSubmenu(Menu, NWBase):
4740 bl_idname = "NODE_MT_nw_switch_shaders_converter_submenu"
4741 bl_label = "Converter"
4743 def draw(self, context):
4744 layout = self.layout
4745 for ident, node_type, rna_name in shaders_converter_nodes_props:
4746 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4747 props.to_type = ident
4750 class NWSwitchShadersLayoutSubmenu(Menu, NWBase):
4751 bl_idname = "NODE_MT_nw_switch_shaders_layout_submenu"
4752 bl_label = "Layout"
4754 def draw(self, context):
4755 layout = self.layout
4756 for ident, node_type, rna_name in shaders_layout_nodes_props:
4757 if node_type != 'FRAME':
4758 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4759 props.to_type = ident
4762 class NWSwitchCompoInputSubmenu(Menu, NWBase):
4763 bl_idname = "NODE_MT_nw_switch_compo_input_submenu"
4764 bl_label = "Input"
4766 def draw(self, context):
4767 layout = self.layout
4768 for ident, node_type, rna_name in compo_input_nodes_props:
4769 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4770 props.to_type = ident
4773 class NWSwitchCompoOutputSubmenu(Menu, NWBase):
4774 bl_idname = "NODE_MT_nw_switch_compo_output_submenu"
4775 bl_label = "Output"
4777 def draw(self, context):
4778 layout = self.layout
4779 for ident, node_type, rna_name in compo_output_nodes_props:
4780 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4781 props.to_type = ident
4784 class NWSwitchCompoColorSubmenu(Menu, NWBase):
4785 bl_idname = "NODE_MT_nw_switch_compo_color_submenu"
4786 bl_label = "Color"
4788 def draw(self, context):
4789 layout = self.layout
4790 for ident, node_type, rna_name in compo_color_nodes_props:
4791 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4792 props.to_type = ident
4795 class NWSwitchCompoConverterSubmenu(Menu, NWBase):
4796 bl_idname = "NODE_MT_nw_switch_compo_converter_submenu"
4797 bl_label = "Converter"
4799 def draw(self, context):
4800 layout = self.layout
4801 for ident, node_type, rna_name in compo_converter_nodes_props:
4802 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4803 props.to_type = ident
4806 class NWSwitchCompoFilterSubmenu(Menu, NWBase):
4807 bl_idname = "NODE_MT_nw_switch_compo_filter_submenu"
4808 bl_label = "Filter"
4810 def draw(self, context):
4811 layout = self.layout
4812 for ident, node_type, rna_name in compo_filter_nodes_props:
4813 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4814 props.to_type = ident
4817 class NWSwitchCompoVectorSubmenu(Menu, NWBase):
4818 bl_idname = "NODE_MT_nw_switch_compo_vector_submenu"
4819 bl_label = "Vector"
4821 def draw(self, context):
4822 layout = self.layout
4823 for ident, node_type, rna_name in compo_vector_nodes_props:
4824 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4825 props.to_type = ident
4828 class NWSwitchCompoMatteSubmenu(Menu, NWBase):
4829 bl_idname = "NODE_MT_nw_switch_compo_matte_submenu"
4830 bl_label = "Matte"
4832 def draw(self, context):
4833 layout = self.layout
4834 for ident, node_type, rna_name in compo_matte_nodes_props:
4835 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4836 props.to_type = ident
4839 class NWSwitchCompoDistortSubmenu(Menu, NWBase):
4840 bl_idname = "NODE_MT_nw_switch_compo_distort_submenu"
4841 bl_label = "Distort"
4843 def draw(self, context):
4844 layout = self.layout
4845 for ident, node_type, rna_name in compo_distort_nodes_props:
4846 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4847 props.to_type = ident
4850 class NWSwitchCompoLayoutSubmenu(Menu, NWBase):
4851 bl_idname = "NODE_MT_nw_switch_compo_layout_submenu"
4852 bl_label = "Layout"
4854 def draw(self, context):
4855 layout = self.layout
4856 for ident, node_type, rna_name in compo_layout_nodes_props:
4857 if node_type != 'FRAME':
4858 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4859 props.to_type = ident
4862 class NWSwitchMatInputSubmenu(Menu, NWBase):
4863 bl_idname = "NODE_MT_nw_switch_mat_input_submenu"
4864 bl_label = "Input"
4866 def draw(self, context):
4867 layout = self.layout
4868 for ident, node_type, rna_name in sorted(blender_mat_input_nodes_props, key=lambda k: k[2]):
4869 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4870 props.to_type = ident
4873 class NWSwitchMatOutputSubmenu(Menu, NWBase):
4874 bl_idname = "NODE_MT_nw_switch_mat_output_submenu"
4875 bl_label = "Output"
4877 def draw(self, context):
4878 layout = self.layout
4879 for ident, node_type, rna_name in sorted(blender_mat_output_nodes_props, key=lambda k: k[2]):
4880 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4881 props.to_type = ident
4884 class NWSwitchMatColorSubmenu(Menu, NWBase):
4885 bl_idname = "NODE_MT_nw_switch_mat_color_submenu"
4886 bl_label = "Color"
4888 def draw(self, context):
4889 layout = self.layout
4890 for ident, node_type, rna_name in sorted(blender_mat_color_nodes_props, key=lambda k: k[2]):
4891 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4892 props.to_type = ident
4895 class NWSwitchMatVectorSubmenu(Menu, NWBase):
4896 bl_idname = "NODE_MT_nw_switch_mat_vector_submenu"
4897 bl_label = "Vector"
4899 def draw(self, context):
4900 layout = self.layout
4901 for ident, node_type, rna_name in sorted(blender_mat_vector_nodes_props, key=lambda k: k[2]):
4902 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4903 props.to_type = ident
4906 class NWSwitchMatConverterSubmenu(Menu, NWBase):
4907 bl_idname = "NODE_MT_nw_switch_mat_converter_submenu"
4908 bl_label = "Converter"
4910 def draw(self, context):
4911 layout = self.layout
4912 for ident, node_type, rna_name in sorted(blender_mat_converter_nodes_props, key=lambda k: k[2]):
4913 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4914 props.to_type = ident
4917 class NWSwitchMatLayoutSubmenu(Menu, NWBase):
4918 bl_idname = "NODE_MT_nw_switch_mat_layout_submenu"
4919 bl_label = "Layout"
4921 def draw(self, context):
4922 layout = self.layout
4923 for ident, node_type, rna_name in sorted(blender_mat_layout_nodes_props, key=lambda k: k[2]):
4924 if node_type != 'FRAME':
4925 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4926 props.to_type = ident
4929 class NWSwitchTexInputSubmenu(Menu, NWBase):
4930 bl_idname = "NODE_MT_nw_switch_tex_input_submenu"
4931 bl_label = "Input"
4933 def draw(self, context):
4934 layout = self.layout
4935 for ident, node_type, rna_name in sorted(texture_input_nodes_props, key=lambda k: k[2]):
4936 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4937 props.to_type = ident
4940 class NWSwitchTexOutputSubmenu(Menu, NWBase):
4941 bl_idname = "NODE_MT_nw_switch_tex_output_submenu"
4942 bl_label = "Output"
4944 def draw(self, context):
4945 layout = self.layout
4946 for ident, node_type, rna_name in sorted(texture_output_nodes_props, key=lambda k: k[2]):
4947 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4948 props.to_type = ident
4951 class NWSwitchTexColorSubmenu(Menu, NWBase):
4952 bl_idname = "NODE_MT_nw_switch_tex_color_submenu"
4953 bl_label = "Color"
4955 def draw(self, context):
4956 layout = self.layout
4957 for ident, node_type, rna_name in sorted(texture_color_nodes_props, key=lambda k: k[2]):
4958 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4959 props.to_type = ident
4962 class NWSwitchTexPatternSubmenu(Menu, NWBase):
4963 bl_idname = "NODE_MT_nw_switch_tex_pattern_submenu"
4964 bl_label = "Pattern"
4966 def draw(self, context):
4967 layout = self.layout
4968 for ident, node_type, rna_name in sorted(texture_pattern_nodes_props, key=lambda k: k[2]):
4969 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4970 props.to_type = ident
4973 class NWSwitchTexTexturesSubmenu(Menu, NWBase):
4974 bl_idname = "NODE_MT_nw_switch_tex_textures_submenu"
4975 bl_label = "Textures"
4977 def draw(self, context):
4978 layout = self.layout
4979 for ident, node_type, rna_name in sorted(texture_textures_nodes_props, key=lambda k: k[2]):
4980 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4981 props.to_type = ident
4984 class NWSwitchTexConverterSubmenu(Menu, NWBase):
4985 bl_idname = "NODE_MT_nw_switch_tex_converter_submenu"
4986 bl_label = "Converter"
4988 def draw(self, context):
4989 layout = self.layout
4990 for ident, node_type, rna_name in sorted(texture_converter_nodes_props, key=lambda k: k[2]):
4991 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
4992 props.to_type = ident
4995 class NWSwitchTexDistortSubmenu(Menu, NWBase):
4996 bl_idname = "NODE_MT_nw_switch_tex_distort_submenu"
4997 bl_label = "Distort"
4999 def draw(self, context):
5000 layout = self.layout
5001 for ident, node_type, rna_name in sorted(texture_distort_nodes_props, key=lambda k: k[2]):
5002 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
5003 props.to_type = ident
5006 class NWSwitchTexLayoutSubmenu(Menu, NWBase):
5007 bl_idname = "NODE_MT_nw_switch_tex_layout_submenu"
5008 bl_label = "Layout"
5010 def draw(self, context):
5011 layout = self.layout
5012 for ident, node_type, rna_name in sorted(texture_layout_nodes_props, key=lambda k: k[2]):
5013 if node_type != 'FRAME':
5014 props = layout.operator(NWSwitchNodeType.bl_idname, text=rna_name)
5015 props.to_type = ident
5017 def draw_switch_category_submenu(self, context):
5018 layout = self.layout
5019 if self.category.name == 'Layout':
5020 for node in self.category.items(context):
5021 if node.nodetype != 'NodeFrame':
5022 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
5023 props.to_type = node.nodetype
5024 else:
5025 for node in self.category.items(context):
5026 if isinstance(node, NodeItemCustom):
5027 node.draw(self, layout, context)
5028 continue
5029 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
5030 props.geo_to_type = node.nodetype
5033 # APPENDAGES TO EXISTING UI
5037 def select_parent_children_buttons(self, context):
5038 layout = self.layout
5039 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
5040 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
5043 def attr_nodes_menu_func(self, context):
5044 col = self.layout.column(align=True)
5045 col.menu("NODE_MT_nw_node_attribute_menu")
5046 col.separator()
5049 def multipleimages_menu_func(self, context):
5050 col = self.layout.column(align=True)
5051 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
5052 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
5053 col.separator()
5056 def bgreset_menu_func(self, context):
5057 self.layout.operator(NWResetBG.bl_idname)
5060 def save_viewer_menu_func(self, context):
5061 if nw_check(context):
5062 if context.space_data.tree_type == 'CompositorNodeTree':
5063 if context.scene.node_tree.nodes.active:
5064 if context.scene.node_tree.nodes.active.type == "VIEWER":
5065 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
5068 def reset_nodes_button(self, context):
5069 node_active = context.active_node
5070 node_selected = context.selected_nodes
5071 node_ignore = ["FRAME","REROUTE", "GROUP"]
5073 # Check if active node is in the selection and respective type
5074 if (len(node_selected) == 1) and node_active.select and node_active.type not in node_ignore:
5075 row = self.layout.row()
5076 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
5077 self.layout.separator()
5079 elif (len(node_selected) == 1) and node_active.select and node_active.type == "FRAME":
5080 row = self.layout.row()
5081 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
5082 self.layout.separator()
5086 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
5088 switch_category_menus = []
5089 addon_keymaps = []
5090 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
5091 # props entry: (property name, property value)
5092 kmi_defs = (
5093 # MERGE NODES
5094 # NWMergeNodes with Ctrl (AUTO).
5095 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
5096 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5097 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
5098 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
5099 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
5100 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5101 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
5102 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
5103 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
5104 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5105 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
5106 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
5107 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
5108 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5109 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
5110 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
5111 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
5112 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5113 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
5114 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
5115 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
5116 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
5117 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
5118 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
5119 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
5120 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
5121 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
5122 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
5123 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5124 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
5125 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
5126 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
5127 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5128 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
5129 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
5130 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
5131 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5132 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
5133 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
5134 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
5135 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5136 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
5137 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
5138 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
5139 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5140 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
5141 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
5142 # NWMergeNodes with Ctrl Shift (MATH)
5143 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
5144 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5145 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
5146 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
5147 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
5148 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5149 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
5150 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
5151 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
5152 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5153 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
5154 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
5155 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
5156 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5157 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
5158 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
5159 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
5160 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
5161 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
5162 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
5163 # BATCH CHANGE NODES
5164 # NWBatchChangeNodes with Alt
5165 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
5166 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5167 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
5168 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
5169 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
5170 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5171 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
5172 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
5173 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
5174 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5175 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
5176 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
5177 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
5178 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5179 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
5180 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
5181 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
5182 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5183 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
5184 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
5185 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
5186 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
5187 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
5188 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
5189 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
5190 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
5191 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
5192 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
5193 # LINK ACTIVE TO SELECTED
5194 # Don't use names, don't replace links (K)
5195 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
5196 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
5197 # Don't use names, replace links (Shift K)
5198 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
5199 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
5200 # Use node name, don't replace links (')
5201 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
5202 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
5203 # Use node name, replace links (Shift ')
5204 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
5205 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
5206 # Don't use names, don't replace links (;)
5207 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
5208 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
5209 # Don't use names, replace links (')
5210 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
5211 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
5212 # CHANGE MIX FACTOR
5213 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
5214 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
5215 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
5216 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
5217 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5218 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5219 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5220 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
5221 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
5222 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
5223 # CLEAR LABEL (Alt L)
5224 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
5225 # MODIFY LABEL (Alt Shift L)
5226 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
5227 # Copy Label from active to selected
5228 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
5229 # DETACH OUTPUTS (Alt Shift D)
5230 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
5231 # LINK TO OUTPUT NODE (O)
5232 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
5233 # SELECT PARENT/CHILDREN
5234 # Select Children
5235 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
5236 # Select Parent
5237 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
5238 # Add Texture Setup
5239 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
5240 # Add Principled BSDF Texture Setup
5241 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
5242 # Reset backdrop
5243 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
5244 # Delete unused
5245 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
5246 # Frame Selected
5247 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
5248 # Swap Outputs
5249 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Outputs"),
5250 # Preview Node
5251 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
5252 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
5253 # Reload Images
5254 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
5255 # Lazy Mix
5256 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
5257 # Lazy Connect
5258 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
5259 # Lazy Connect with Menu
5260 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
5261 # Viewer Tile Center
5262 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
5263 # Align Nodes
5264 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
5265 # Reset Nodes (Back Space)
5266 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
5267 # MENUS
5268 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
5269 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
5270 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
5271 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
5272 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
5273 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
5277 classes = (
5278 NWPrincipledPreferences,
5279 NWNodeWrangler,
5280 NWLazyMix,
5281 NWLazyConnect,
5282 NWDeleteUnused,
5283 NWSwapLinks,
5284 NWResetBG,
5285 NWAddAttrNode,
5286 NWPreviewNode,
5287 NWFrameSelected,
5288 NWReloadImages,
5289 NWSwitchNodeType,
5290 NWMergeNodes,
5291 NWBatchChangeNodes,
5292 NWChangeMixFactor,
5293 NWCopySettings,
5294 NWCopyLabel,
5295 NWClearLabel,
5296 NWModifyLabels,
5297 NWAddTextureSetup,
5298 NWAddPrincipledSetup,
5299 NWAddReroutes,
5300 NWLinkActiveToSelected,
5301 NWAlignNodes,
5302 NWSelectParentChildren,
5303 NWDetachOutputs,
5304 NWLinkToOutputNode,
5305 NWMakeLink,
5306 NWCallInputsMenu,
5307 NWAddSequence,
5308 NWAddMultipleImages,
5309 NWViewerFocus,
5310 NWSaveViewer,
5311 NWResetNodes,
5312 NodeWranglerPanel,
5313 NodeWranglerMenu,
5314 NWMergeNodesMenu,
5315 NWMergeShadersMenu,
5316 NWMergeGeometryMenu,
5317 NWMergeMixMenu,
5318 NWConnectionListOutputs,
5319 NWConnectionListInputs,
5320 NWMergeMathMenu,
5321 NWBatchChangeNodesMenu,
5322 NWBatchChangeBlendTypeMenu,
5323 NWBatchChangeOperationMenu,
5324 NWCopyToSelectedMenu,
5325 NWCopyLabelMenu,
5326 NWAddReroutesMenu,
5327 NWLinkActiveToSelectedMenu,
5328 NWLinkStandardMenu,
5329 NWLinkUseNodeNameMenu,
5330 NWLinkUseOutputsNamesMenu,
5331 NWAttributeMenu,
5332 NWSwitchNodeTypeMenu,
5333 NWSwitchShadersInputSubmenu,
5334 NWSwitchShadersOutputSubmenu,
5335 NWSwitchShadersShaderSubmenu,
5336 NWSwitchShadersTextureSubmenu,
5337 NWSwitchShadersColorSubmenu,
5338 NWSwitchShadersVectorSubmenu,
5339 NWSwitchShadersConverterSubmenu,
5340 NWSwitchShadersLayoutSubmenu,
5341 NWSwitchCompoInputSubmenu,
5342 NWSwitchCompoOutputSubmenu,
5343 NWSwitchCompoColorSubmenu,
5344 NWSwitchCompoConverterSubmenu,
5345 NWSwitchCompoFilterSubmenu,
5346 NWSwitchCompoVectorSubmenu,
5347 NWSwitchCompoMatteSubmenu,
5348 NWSwitchCompoDistortSubmenu,
5349 NWSwitchCompoLayoutSubmenu,
5350 NWSwitchMatInputSubmenu,
5351 NWSwitchMatOutputSubmenu,
5352 NWSwitchMatColorSubmenu,
5353 NWSwitchMatVectorSubmenu,
5354 NWSwitchMatConverterSubmenu,
5355 NWSwitchMatLayoutSubmenu,
5356 NWSwitchTexInputSubmenu,
5357 NWSwitchTexOutputSubmenu,
5358 NWSwitchTexColorSubmenu,
5359 NWSwitchTexPatternSubmenu,
5360 NWSwitchTexTexturesSubmenu,
5361 NWSwitchTexConverterSubmenu,
5362 NWSwitchTexDistortSubmenu,
5363 NWSwitchTexLayoutSubmenu,
5366 def register():
5367 from bpy.utils import register_class
5369 # props
5370 bpy.types.Scene.NWBusyDrawing = StringProperty(
5371 name="Busy Drawing!",
5372 default="",
5373 description="An internal property used to store only the first mouse position")
5374 bpy.types.Scene.NWLazySource = StringProperty(
5375 name="Lazy Source!",
5376 default="x",
5377 description="An internal property used to store the first node in a Lazy Connect operation")
5378 bpy.types.Scene.NWLazyTarget = StringProperty(
5379 name="Lazy Target!",
5380 default="x",
5381 description="An internal property used to store the last node in a Lazy Connect operation")
5382 bpy.types.Scene.NWSourceSocket = IntProperty(
5383 name="Source Socket!",
5384 default=0,
5385 description="An internal property used to store the source socket in a Lazy Connect operation")
5386 bpy.types.NodeSocketInterface.NWViewerSocket = BoolProperty(
5387 name="NW Socket",
5388 default=False,
5389 description="An internal property used to determine if a socket is generated by the addon"
5392 for cls in classes:
5393 register_class(cls)
5395 # keymaps
5396 addon_keymaps.clear()
5397 kc = bpy.context.window_manager.keyconfigs.addon
5398 if kc:
5399 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
5400 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
5401 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
5402 if props:
5403 for prop, value in props:
5404 setattr(kmi.properties, prop, value)
5405 addon_keymaps.append((km, kmi))
5407 # menu items
5408 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
5409 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
5410 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
5411 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
5412 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
5413 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
5414 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
5415 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
5417 # switch submenus
5418 switch_category_menus.clear()
5419 for cat in node_categories_iter(None):
5420 if cat.name not in ['Group', 'Script'] and cat.identifier.startswith('GEO'):
5421 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
5422 switch_category_type = type(idname, (bpy.types.Menu,), {
5423 "bl_space_type": 'NODE_EDITOR',
5424 "bl_label": cat.name,
5425 "category": cat,
5426 "poll": cat.poll,
5427 "draw": draw_switch_category_submenu,
5430 switch_category_menus.append(switch_category_type)
5432 bpy.utils.register_class(switch_category_type)
5435 def unregister():
5436 from bpy.utils import unregister_class
5438 # props
5439 del bpy.types.Scene.NWBusyDrawing
5440 del bpy.types.Scene.NWLazySource
5441 del bpy.types.Scene.NWLazyTarget
5442 del bpy.types.Scene.NWSourceSocket
5443 del bpy.types.NodeSocketInterface.NWViewerSocket
5445 for cat_types in switch_category_menus:
5446 bpy.utils.unregister_class(cat_types)
5447 switch_category_menus.clear()
5449 # keymaps
5450 for km, kmi in addon_keymaps:
5451 km.keymap_items.remove(kmi)
5452 addon_keymaps.clear()
5454 # menuitems
5455 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
5456 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
5457 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
5458 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
5459 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
5460 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
5461 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
5462 bpy.types.NODE_MT_node.remove(reset_nodes_button)
5464 for cls in classes:
5465 unregister_class(cls)
5467 if __name__ == "__main__":
5468 register()