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