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