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