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