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