glTF importer: add hook before gltf import
[blender-addons.git] / node_wrangler.py
blob5932388ff05fbd350c6e4a2a595650d4ccb4c92c
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, 43),
7 "blender": (3, 4, 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
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 # list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
76 # used list, not tuple for easy merging with other lists.
77 blend_types = [
78 ('MIX', 'Mix', 'Mix Mode'),
79 ('ADD', 'Add', 'Add Mode'),
80 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
81 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
82 ('SCREEN', 'Screen', 'Screen Mode'),
83 ('DIVIDE', 'Divide', 'Divide Mode'),
84 ('DIFFERENCE', 'Difference', 'Difference Mode'),
85 ('DARKEN', 'Darken', 'Darken Mode'),
86 ('LIGHTEN', 'Lighten', 'Lighten Mode'),
87 ('OVERLAY', 'Overlay', 'Overlay Mode'),
88 ('DODGE', 'Dodge', 'Dodge Mode'),
89 ('BURN', 'Burn', 'Burn Mode'),
90 ('HUE', 'Hue', 'Hue Mode'),
91 ('SATURATION', 'Saturation', 'Saturation Mode'),
92 ('VALUE', 'Value', 'Value Mode'),
93 ('COLOR', 'Color', 'Color Mode'),
94 ('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
95 ('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
98 # list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
99 # used list, not tuple for easy merging with other lists.
100 operations = [
101 ('ADD', 'Add', 'Add Mode'),
102 ('SUBTRACT', 'Subtract', 'Subtract Mode'),
103 ('MULTIPLY', 'Multiply', 'Multiply Mode'),
104 ('DIVIDE', 'Divide', 'Divide Mode'),
105 ('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
106 ('SINE', 'Sine', 'Sine Mode'),
107 ('COSINE', 'Cosine', 'Cosine Mode'),
108 ('TANGENT', 'Tangent', 'Tangent Mode'),
109 ('ARCSINE', 'Arcsine', 'Arcsine Mode'),
110 ('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
111 ('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
112 ('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
113 ('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
114 ('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
115 ('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
116 ('POWER', 'Power', 'Power Mode'),
117 ('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
118 ('SQRT', 'Square Root', 'Square Root Mode'),
119 ('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
120 ('EXPONENT', 'Exponent', 'Exponent Mode'),
121 ('MINIMUM', 'Minimum', 'Minimum Mode'),
122 ('MAXIMUM', 'Maximum', 'Maximum Mode'),
123 ('LESS_THAN', 'Less Than', 'Less Than Mode'),
124 ('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
125 ('SIGN', 'Sign', 'Sign Mode'),
126 ('COMPARE', 'Compare', 'Compare Mode'),
127 ('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
128 ('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
129 ('FRACT', 'Fraction', 'Fraction Mode'),
130 ('MODULO', 'Modulo', 'Modulo Mode'),
131 ('SNAP', 'Snap', 'Snap Mode'),
132 ('WRAP', 'Wrap', 'Wrap Mode'),
133 ('PINGPONG', 'Pingpong', 'Pingpong Mode'),
134 ('ABSOLUTE', 'Absolute', 'Absolute Mode'),
135 ('ROUND', 'Round', 'Round Mode'),
136 ('FLOOR', 'Floor', 'Floor Mode'),
137 ('CEIL', 'Ceil', 'Ceil Mode'),
138 ('TRUNCATE', 'Truncate', 'Truncate Mode'),
139 ('RADIANS', 'To Radians', 'To Radians Mode'),
140 ('DEGREES', 'To Degrees', 'To Degrees Mode'),
143 # Operations used by the geometry boolean node and join geometry node
144 geo_combine_operations = [
145 ('JOIN', 'Join Geometry', 'Join Geometry Mode'),
146 ('INTERSECT', 'Intersect', 'Intersect Mode'),
147 ('UNION', 'Union', 'Union Mode'),
148 ('DIFFERENCE', 'Difference', 'Difference Mode'),
151 # in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
152 # used list, not tuple for easy merging with other lists.
153 navs = [
154 ('CURRENT', 'Current', 'Leave at current state'),
155 ('NEXT', 'Next', 'Next blend type/operation'),
156 ('PREV', 'Prev', 'Previous blend type/operation'),
159 draw_color_sets = {
160 "red_white": (
161 (1.0, 1.0, 1.0, 0.7),
162 (1.0, 0.0, 0.0, 0.7),
163 (0.8, 0.2, 0.2, 1.0)
165 "green": (
166 (0.0, 0.0, 0.0, 1.0),
167 (0.38, 0.77, 0.38, 1.0),
168 (0.38, 0.77, 0.38, 1.0)
170 "yellow": (
171 (0.0, 0.0, 0.0, 1.0),
172 (0.77, 0.77, 0.16, 1.0),
173 (0.77, 0.77, 0.16, 1.0)
175 "purple": (
176 (0.0, 0.0, 0.0, 1.0),
177 (0.38, 0.38, 0.77, 1.0),
178 (0.38, 0.38, 0.77, 1.0)
180 "grey": (
181 (0.0, 0.0, 0.0, 1.0),
182 (0.63, 0.63, 0.63, 1.0),
183 (0.63, 0.63, 0.63, 1.0)
185 "black": (
186 (1.0, 1.0, 1.0, 0.7),
187 (0.0, 0.0, 0.0, 0.7),
188 (0.2, 0.2, 0.2, 1.0)
192 viewer_socket_name = "tmp_viewer"
194 def get_nodes_from_category(category_name, context):
195 for category in node_categories_iter(context):
196 if category.name == category_name:
197 return sorted(category.items(context), key=lambda node: node.label)
199 def get_first_enabled_output(node):
200 for output in node.outputs:
201 if output.enabled:
202 return output
203 else:
204 return node.outputs[0]
206 def is_visible_socket(socket):
207 return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
209 def nice_hotkey_name(punc):
210 # convert the ugly string name into the actual character
211 nice_name = {
212 'LEFTMOUSE': "LMB",
213 'MIDDLEMOUSE': "MMB",
214 'RIGHTMOUSE': "RMB",
215 'WHEELUPMOUSE': "Wheel Up",
216 'WHEELDOWNMOUSE': "Wheel Down",
217 'WHEELINMOUSE': "Wheel In",
218 'WHEELOUTMOUSE': "Wheel Out",
219 'ZERO': "0",
220 'ONE': "1",
221 'TWO': "2",
222 'THREE': "3",
223 'FOUR': "4",
224 'FIVE': "5",
225 'SIX': "6",
226 'SEVEN': "7",
227 'EIGHT': "8",
228 'NINE': "9",
229 'OSKEY': "Super",
230 'RET': "Enter",
231 'LINE_FEED': "Enter",
232 'SEMI_COLON': ";",
233 'PERIOD': ".",
234 'COMMA': ",",
235 'QUOTE': '"',
236 'MINUS': "-",
237 'SLASH': "/",
238 'BACK_SLASH': "\\",
239 'EQUAL': "=",
240 'NUMPAD_1': "Numpad 1",
241 'NUMPAD_2': "Numpad 2",
242 'NUMPAD_3': "Numpad 3",
243 'NUMPAD_4': "Numpad 4",
244 'NUMPAD_5': "Numpad 5",
245 'NUMPAD_6': "Numpad 6",
246 'NUMPAD_7': "Numpad 7",
247 'NUMPAD_8': "Numpad 8",
248 'NUMPAD_9': "Numpad 9",
249 'NUMPAD_0': "Numpad 0",
250 'NUMPAD_PERIOD': "Numpad .",
251 'NUMPAD_SLASH': "Numpad /",
252 'NUMPAD_ASTERIX': "Numpad *",
253 'NUMPAD_MINUS': "Numpad -",
254 'NUMPAD_ENTER': "Numpad Enter",
255 'NUMPAD_PLUS': "Numpad +",
257 try:
258 return nice_name[punc]
259 except KeyError:
260 return punc.replace("_", " ").title()
263 def force_update(context):
264 context.space_data.node_tree.update_tag()
267 def dpi_fac():
268 prefs = bpy.context.preferences.system
269 return prefs.dpi / 72
272 def prefs_line_width():
273 prefs = bpy.context.preferences.system
274 return prefs.pixel_size
277 def node_mid_pt(node, axis):
278 if axis == 'x':
279 d = node.location.x + (node.dimensions.x / 2)
280 elif axis == 'y':
281 d = node.location.y - (node.dimensions.y / 2)
282 else:
283 d = 0
284 return d
287 def autolink(node1, node2, links):
288 link_made = False
289 available_inputs = [inp for inp in node2.inputs if inp.enabled]
290 available_outputs = [outp for outp in node1.outputs if outp.enabled]
291 for outp in available_outputs:
292 for inp in available_inputs:
293 if not inp.is_linked and inp.name == outp.name:
294 link_made = True
295 links.new(outp, inp)
296 return True
298 for outp in available_outputs:
299 for inp in available_inputs:
300 if not inp.is_linked and inp.type == outp.type:
301 link_made = True
302 links.new(outp, inp)
303 return True
305 # force some connection even if the type doesn't match
306 if available_outputs:
307 for inp in available_inputs:
308 if not inp.is_linked:
309 link_made = True
310 links.new(available_outputs[0], inp)
311 return True
313 # even if no sockets are open, force one of matching type
314 for outp in available_outputs:
315 for inp in available_inputs:
316 if inp.type == outp.type:
317 link_made = True
318 links.new(outp, inp)
319 return True
321 # do something!
322 for outp in available_outputs:
323 for inp in available_inputs:
324 link_made = True
325 links.new(outp, inp)
326 return True
328 print("Could not make a link from " + node1.name + " to " + node2.name)
329 return link_made
331 def abs_node_location(node):
332 abs_location = node.location
333 if node.parent is None:
334 return abs_location
335 return abs_location + abs_node_location(node.parent)
337 def node_at_pos(nodes, context, event):
338 nodes_under_mouse = []
339 target_node = None
341 store_mouse_cursor(context, event)
342 x, y = context.space_data.cursor_location
344 # Make a list of each corner (and middle of border) for each node.
345 # Will be sorted to find nearest point and thus nearest node
346 node_points_with_dist = []
347 for node in nodes:
348 skipnode = False
349 if node.type != 'FRAME': # no point trying to link to a frame node
350 dimx = node.dimensions.x / dpi_fac()
351 dimy = node.dimensions.y / dpi_fac()
352 locx, locy = abs_node_location(node)
354 if not skipnode:
355 node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
356 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
357 node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
358 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
360 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
361 node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
362 node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
363 node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
365 nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
367 for node in nodes:
368 if node.type != 'FRAME' and skipnode == False:
369 locx, locy = abs_node_location(node)
370 dimx = node.dimensions.x / dpi_fac()
371 dimy = node.dimensions.y / dpi_fac()
372 if (locx <= x <= locx + dimx) and \
373 (locy - dimy <= y <= locy):
374 nodes_under_mouse.append(node)
376 if len(nodes_under_mouse) == 1:
377 if nodes_under_mouse[0] != nearest_node:
378 target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
379 else:
380 target_node = nearest_node # else use the nearest node
381 else:
382 target_node = nearest_node
383 return target_node
386 def store_mouse_cursor(context, event):
387 space = context.space_data
388 v2d = context.region.view2d
389 tree = space.edit_tree
391 # convert mouse position to the View2D for later node placement
392 if context.region.type == 'WINDOW':
393 space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
394 else:
395 space.cursor_location = tree.view_center
397 def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
398 shader = gpu.shader.from_builtin('POLYLINE_SMOOTH_COLOR')
399 shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
400 shader.uniform_float("lineWidth", size * prefs_line_width())
402 vertices = ((x1, y1), (x2, y2))
403 vertex_colors = ((colour[0]+(1.0-colour[0])/4,
404 colour[1]+(1.0-colour[1])/4,
405 colour[2]+(1.0-colour[2])/4,
406 colour[3]+(1.0-colour[3])/4),
407 colour)
409 batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
410 batch.draw(shader)
413 def draw_circle_2d_filled(mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
414 radius = radius * prefs_line_width()
415 sides = 12
416 vertices = [(radius * cos(i * 2 * pi / sides) + mx,
417 radius * sin(i * 2 * pi / sides) + my)
418 for i in range(sides + 1)]
420 shader = gpu.shader.from_builtin('UNIFORM_COLOR')
421 shader.uniform_float("color", colour)
422 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
423 batch.draw(shader)
426 def draw_rounded_node_border(node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
427 area_width = bpy.context.area.width
428 sides = 16
429 radius *= prefs_line_width()
431 nlocx, nlocy = abs_node_location(node)
433 nlocx = (nlocx+1) * dpi_fac()
434 nlocy = (nlocy+1) * dpi_fac()
435 ndimx = node.dimensions.x
436 ndimy = node.dimensions.y
438 if node.hide:
439 nlocx += -1
440 nlocy += 5
441 if node.type == 'REROUTE':
442 #nlocx += 1
443 nlocy -= 1
444 ndimx = 0
445 ndimy = 0
446 radius += 6
448 shader = gpu.shader.from_builtin('UNIFORM_COLOR')
449 shader.uniform_float("color", colour)
451 # Top left corner
452 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
453 vertices = [(mx,my)]
454 for i in range(sides+1):
455 if (4<=i<=8):
456 if mx < area_width:
457 cosine = radius * cos(i * 2 * pi / sides) + mx
458 sine = radius * sin(i * 2 * pi / sides) + my
459 vertices.append((cosine,sine))
461 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
462 batch.draw(shader)
464 # Top right corner
465 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
466 vertices = [(mx,my)]
467 for i in range(sides+1):
468 if (0<=i<=4):
469 if mx < area_width:
470 cosine = radius * cos(i * 2 * pi / sides) + mx
471 sine = radius * sin(i * 2 * pi / sides) + my
472 vertices.append((cosine,sine))
474 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
475 batch.draw(shader)
477 # Bottom left corner
478 mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
479 vertices = [(mx,my)]
480 for i in range(sides+1):
481 if (8<=i<=12):
482 if mx < area_width:
483 cosine = radius * cos(i * 2 * pi / sides) + mx
484 sine = radius * sin(i * 2 * pi / sides) + my
485 vertices.append((cosine,sine))
487 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
488 batch.draw(shader)
490 # Bottom right corner
491 mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
492 vertices = [(mx,my)]
493 for i in range(sides+1):
494 if (12<=i<=16):
495 if mx < area_width:
496 cosine = radius * cos(i * 2 * pi / sides) + mx
497 sine = radius * sin(i * 2 * pi / sides) + my
498 vertices.append((cosine,sine))
500 batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
501 batch.draw(shader)
503 # prepare drawing all edges in one batch
504 vertices = []
505 indices = []
506 id_last = 0
508 # Left edge
509 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
510 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
511 if m1x < area_width and m2x < area_width:
512 vertices.extend([(m2x-radius,m2y), (m2x,m2y),
513 (m1x,m1y), (m1x-radius,m1y)])
514 indices.extend([(id_last, id_last+1, id_last+3),
515 (id_last+3, id_last+1, id_last+2)])
516 id_last += 4
518 # Top edge
519 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
520 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
521 m1x = min(m1x, area_width)
522 m2x = min(m2x, area_width)
523 vertices.extend([(m1x,m1y), (m2x,m1y),
524 (m2x,m1y+radius), (m1x,m1y+radius)])
525 indices.extend([(id_last, id_last+1, id_last+3),
526 (id_last+3, id_last+1, id_last+2)])
527 id_last += 4
529 # Right edge
530 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
531 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
532 if m1x < area_width and m2x < area_width:
533 vertices.extend([(m1x,m2y), (m1x+radius,m2y),
534 (m1x+radius,m1y), (m1x,m1y)])
535 indices.extend([(id_last, id_last+1, id_last+3),
536 (id_last+3, id_last+1, id_last+2)])
537 id_last += 4
539 # Bottom edge
540 m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy-ndimy, clip=False)
541 m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy-ndimy, clip=False)
542 m1x = min(m1x, area_width)
543 m2x = min(m2x, area_width)
544 vertices.extend([(m1x,m2y), (m2x,m2y),
545 (m2x,m1y-radius), (m1x,m1y-radius)])
546 indices.extend([(id_last, id_last+1, id_last+3),
547 (id_last+3, id_last+1, id_last+2)])
549 # now draw all edges in one batch
550 if len(vertices) != 0:
551 batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
552 batch.draw(shader)
554 def draw_callback_nodeoutline(self, context, mode):
555 if self.mouse_path:
556 gpu.state.blend_set('ALPHA')
558 nodes, links = get_nodes_links(context)
560 if mode == "LINK":
561 col_outer = (1.0, 0.2, 0.2, 0.4)
562 col_inner = (0.0, 0.0, 0.0, 0.5)
563 col_circle_inner = (0.3, 0.05, 0.05, 1.0)
564 elif mode == "LINKMENU":
565 col_outer = (0.4, 0.6, 1.0, 0.4)
566 col_inner = (0.0, 0.0, 0.0, 0.5)
567 col_circle_inner = (0.08, 0.15, .3, 1.0)
568 elif mode == "MIX":
569 col_outer = (0.2, 1.0, 0.2, 0.4)
570 col_inner = (0.0, 0.0, 0.0, 0.5)
571 col_circle_inner = (0.05, 0.3, 0.05, 1.0)
573 m1x = self.mouse_path[0][0]
574 m1y = self.mouse_path[0][1]
575 m2x = self.mouse_path[-1][0]
576 m2y = self.mouse_path[-1][1]
578 n1 = nodes[context.scene.NWLazySource]
579 n2 = nodes[context.scene.NWLazyTarget]
581 if n1 == n2:
582 col_outer = (0.4, 0.4, 0.4, 0.4)
583 col_inner = (0.0, 0.0, 0.0, 0.5)
584 col_circle_inner = (0.2, 0.2, 0.2, 1.0)
586 draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
587 draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
588 draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
589 draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
591 draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
592 draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
594 # circle outline
595 draw_circle_2d_filled(m1x, m1y, 7, col_outer)
596 draw_circle_2d_filled(m2x, m2y, 7, col_outer)
598 # circle inner
599 draw_circle_2d_filled(m1x, m1y, 5, col_circle_inner)
600 draw_circle_2d_filled(m2x, m2y, 5, col_circle_inner)
602 gpu.state.blend_set('NONE')
604 def get_active_tree(context):
605 tree = context.space_data.node_tree
606 path = []
607 # Get nodes from currently edited tree.
608 # If user is editing a group, space_data.node_tree is still the base level (outside group).
609 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
610 # the same as context.active_node, the user is in a group.
611 # Check recursively until we find the real active node_tree:
612 if tree.nodes.active:
613 while tree.nodes.active != context.active_node:
614 tree = tree.nodes.active.node_tree
615 path.append(tree)
616 return tree, path
618 def get_nodes_links(context):
619 tree, path = get_active_tree(context)
620 return tree.nodes, tree.links
622 def is_viewer_socket(socket):
623 # checks if a internal socket is a valid viewer socket
624 return socket.name == viewer_socket_name and socket.NWViewerSocket
626 def get_internal_socket(socket):
627 #get the internal socket from a socket inside or outside the group
628 node = socket.node
629 if node.type == 'GROUP_OUTPUT':
630 source_iterator = node.inputs
631 iterator = node.id_data.outputs
632 elif node.type == 'GROUP_INPUT':
633 source_iterator = node.outputs
634 iterator = node.id_data.inputs
635 elif hasattr(node, "node_tree"):
636 if socket.is_output:
637 source_iterator = node.outputs
638 iterator = node.node_tree.outputs
639 else:
640 source_iterator = node.inputs
641 iterator = node.node_tree.inputs
642 else:
643 return None
645 for i, s in enumerate(source_iterator):
646 if s == socket:
647 break
648 return iterator[i]
650 def is_viewer_link(link, output_node):
651 if link.to_node == output_node and link.to_socket == output_node.inputs[0]:
652 return True
653 if link.to_node.type == 'GROUP_OUTPUT':
654 socket = get_internal_socket(link.to_socket)
655 if is_viewer_socket(socket):
656 return True
657 return False
659 def get_group_output_node(tree):
660 for node in tree.nodes:
661 if node.type == 'GROUP_OUTPUT' and node.is_active_output == True:
662 return node
664 def get_output_location(tree):
665 # get right-most location
666 sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
667 max_xloc_node = sorted_by_xloc[-1]
669 # get average y location
670 sum_yloc = 0
671 for node in tree.nodes:
672 sum_yloc += node.location.y
674 loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
675 loc_y = sum_yloc / len(tree.nodes)
676 return loc_x, loc_y
678 # Principled prefs
679 class NWPrincipledPreferences(bpy.types.PropertyGroup):
680 base_color: StringProperty(
681 name='Base Color',
682 default='diffuse diff albedo base col color',
683 description='Naming Components for Base Color maps')
684 sss_color: StringProperty(
685 name='Subsurface Color',
686 default='sss subsurface',
687 description='Naming Components for Subsurface Color maps')
688 metallic: StringProperty(
689 name='Metallic',
690 default='metallic metalness metal mtl',
691 description='Naming Components for metallness maps')
692 specular: StringProperty(
693 name='Specular',
694 default='specularity specular spec spc',
695 description='Naming Components for Specular maps')
696 normal: StringProperty(
697 name='Normal',
698 default='normal nor nrm nrml norm',
699 description='Naming Components for Normal maps')
700 bump: StringProperty(
701 name='Bump',
702 default='bump bmp',
703 description='Naming Components for bump maps')
704 rough: StringProperty(
705 name='Roughness',
706 default='roughness rough rgh',
707 description='Naming Components for roughness maps')
708 gloss: StringProperty(
709 name='Gloss',
710 default='gloss glossy glossiness',
711 description='Naming Components for glossy maps')
712 displacement: StringProperty(
713 name='Displacement',
714 default='displacement displace disp dsp height heightmap',
715 description='Naming Components for displacement maps')
716 transmission: StringProperty(
717 name='Transmission',
718 default='transmission transparency',
719 description='Naming Components for transmission maps')
720 emission: StringProperty(
721 name='Emission',
722 default='emission emissive emit',
723 description='Naming Components for emission maps')
724 alpha: StringProperty(
725 name='Alpha',
726 default='alpha opacity',
727 description='Naming Components for alpha maps')
728 ambient_occlusion: StringProperty(
729 name='Ambient Occlusion',
730 default='ao ambient occlusion',
731 description='Naming Components for AO maps')
733 # Addon prefs
734 class NWNodeWrangler(bpy.types.AddonPreferences):
735 bl_idname = __name__
737 merge_hide: EnumProperty(
738 name="Hide Mix nodes",
739 items=(
740 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
741 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
742 ("NEVER", "Never", "Never collapse the new merge nodes")
744 default='NON_SHADER',
745 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
746 merge_position: EnumProperty(
747 name="Mix Node Position",
748 items=(
749 ("CENTER", "Center", "Place the Mix node between the two nodes"),
750 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
752 default='CENTER',
753 description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
755 show_hotkey_list: BoolProperty(
756 name="Show Hotkey List",
757 default=False,
758 description="Expand this box into a list of all the hotkeys for functions in this addon"
760 hotkey_list_filter: StringProperty(
761 name=" Filter by Name",
762 default="",
763 description="Show only hotkeys that have this text in their name",
764 options={'TEXTEDIT_UPDATE'}
766 show_principled_lists: BoolProperty(
767 name="Show Principled naming tags",
768 default=False,
769 description="Expand this box into a list of all naming tags for principled texture setup"
771 principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
773 def draw(self, context):
774 layout = self.layout
775 col = layout.column()
776 col.prop(self, "merge_position")
777 col.prop(self, "merge_hide")
779 box = layout.box()
780 col = box.column(align=True)
781 col.prop(self, "show_principled_lists", text='Edit tags for auto texture detection in Principled BSDF setup', toggle=True)
782 if self.show_principled_lists:
783 tags = self.principled_tags
785 col.prop(tags, "base_color")
786 col.prop(tags, "sss_color")
787 col.prop(tags, "metallic")
788 col.prop(tags, "specular")
789 col.prop(tags, "rough")
790 col.prop(tags, "gloss")
791 col.prop(tags, "normal")
792 col.prop(tags, "bump")
793 col.prop(tags, "displacement")
794 col.prop(tags, "transmission")
795 col.prop(tags, "emission")
796 col.prop(tags, "alpha")
797 col.prop(tags, "ambient_occlusion")
799 box = layout.box()
800 col = box.column(align=True)
801 hotkey_button_name = "Show Hotkey List"
802 if self.show_hotkey_list:
803 hotkey_button_name = "Hide Hotkey List"
804 col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
805 if self.show_hotkey_list:
806 col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
807 col.separator()
808 for hotkey in kmi_defs:
809 if hotkey[7]:
810 hotkey_name = hotkey[7]
812 if self.hotkey_list_filter.lower() in hotkey_name.lower():
813 row = col.row(align=True)
814 row.label(text=hotkey_name)
815 keystr = nice_hotkey_name(hotkey[1])
816 if hotkey[4]:
817 keystr = "Shift " + keystr
818 if hotkey[5]:
819 keystr = "Alt " + keystr
820 if hotkey[3]:
821 keystr = "Ctrl " + keystr
822 row.label(text=keystr)
826 def nw_check(context):
827 space = context.space_data
828 valid_trees = ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
830 if (space.type == 'NODE_EDITOR'
831 and space.node_tree is not None
832 and space.node_tree.library is None
833 and space.tree_type in valid_trees):
834 return True
836 return False
838 class NWBase:
839 @classmethod
840 def poll(cls, context):
841 return nw_check(context)
844 # OPERATORS
845 class NWLazyMix(Operator, NWBase):
846 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
847 bl_idname = "node.nw_lazy_mix"
848 bl_label = "Mix Nodes"
849 bl_options = {'REGISTER', 'UNDO'}
851 def modal(self, context, event):
852 context.area.tag_redraw()
853 nodes, links = get_nodes_links(context)
854 cont = True
856 start_pos = [event.mouse_region_x, event.mouse_region_y]
858 node1 = None
859 if not context.scene.NWBusyDrawing:
860 node1 = node_at_pos(nodes, context, event)
861 if node1:
862 context.scene.NWBusyDrawing = node1.name
863 else:
864 if context.scene.NWBusyDrawing != 'STOP':
865 node1 = nodes[context.scene.NWBusyDrawing]
867 context.scene.NWLazySource = node1.name
868 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
870 if event.type == 'MOUSEMOVE':
871 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
873 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
874 end_pos = [event.mouse_region_x, event.mouse_region_y]
875 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
877 node2 = None
878 node2 = node_at_pos(nodes, context, event)
879 if node2:
880 context.scene.NWBusyDrawing = node2.name
882 if node1 == node2:
883 cont = False
885 if cont:
886 if node1 and node2:
887 for node in nodes:
888 node.select = False
889 node1.select = True
890 node2.select = True
892 bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
894 context.scene.NWBusyDrawing = ""
895 return {'FINISHED'}
897 elif event.type == 'ESC':
898 print('cancelled')
899 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
900 return {'CANCELLED'}
902 return {'RUNNING_MODAL'}
904 def invoke(self, context, event):
905 if context.area.type == 'NODE_EDITOR':
906 # the arguments we pass the the callback
907 args = (self, context, 'MIX')
908 # Add the region OpenGL drawing callback
909 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
910 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
912 self.mouse_path = []
914 context.window_manager.modal_handler_add(self)
915 return {'RUNNING_MODAL'}
916 else:
917 self.report({'WARNING'}, "View3D not found, cannot run operator")
918 return {'CANCELLED'}
921 class NWLazyConnect(Operator, NWBase):
922 """Connect two nodes without clicking a specific socket (automatically determined"""
923 bl_idname = "node.nw_lazy_connect"
924 bl_label = "Lazy Connect"
925 bl_options = {'REGISTER', 'UNDO'}
926 with_menu: BoolProperty()
928 def modal(self, context, event):
929 context.area.tag_redraw()
930 nodes, links = get_nodes_links(context)
931 cont = True
933 start_pos = [event.mouse_region_x, event.mouse_region_y]
935 node1 = None
936 if not context.scene.NWBusyDrawing:
937 node1 = node_at_pos(nodes, context, event)
938 if node1:
939 context.scene.NWBusyDrawing = node1.name
940 else:
941 if context.scene.NWBusyDrawing != 'STOP':
942 node1 = nodes[context.scene.NWBusyDrawing]
944 context.scene.NWLazySource = node1.name
945 context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
947 if event.type == 'MOUSEMOVE':
948 self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
950 elif event.type == 'RIGHTMOUSE' and event.value == 'RELEASE':
951 end_pos = [event.mouse_region_x, event.mouse_region_y]
952 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
954 node2 = None
955 node2 = node_at_pos(nodes, context, event)
956 if node2:
957 context.scene.NWBusyDrawing = node2.name
959 if node1 == node2:
960 cont = False
962 link_success = False
963 if cont:
964 if node1 and node2:
965 original_sel = []
966 original_unsel = []
967 for node in nodes:
968 if node.select == True:
969 node.select = False
970 original_sel.append(node)
971 else:
972 original_unsel.append(node)
973 node1.select = True
974 node2.select = True
976 #link_success = autolink(node1, node2, links)
977 if self.with_menu:
978 if len(node1.outputs) > 1 and node2.inputs:
979 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
980 elif len(node1.outputs) == 1:
981 bpy.ops.node.nw_call_inputs_menu(from_socket=0)
982 else:
983 link_success = autolink(node1, node2, links)
985 for node in original_sel:
986 node.select = True
987 for node in original_unsel:
988 node.select = False
990 if link_success:
991 force_update(context)
992 context.scene.NWBusyDrawing = ""
993 return {'FINISHED'}
995 elif event.type == 'ESC':
996 bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
997 return {'CANCELLED'}
999 return {'RUNNING_MODAL'}
1001 def invoke(self, context, event):
1002 if context.area.type == 'NODE_EDITOR':
1003 nodes, links = get_nodes_links(context)
1004 node = node_at_pos(nodes, context, event)
1005 if node:
1006 context.scene.NWBusyDrawing = node.name
1008 # the arguments we pass the the callback
1009 mode = "LINK"
1010 if self.with_menu:
1011 mode = "LINKMENU"
1012 args = (self, context, mode)
1013 # Add the region OpenGL drawing callback
1014 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1015 self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
1017 self.mouse_path = []
1019 context.window_manager.modal_handler_add(self)
1020 return {'RUNNING_MODAL'}
1021 else:
1022 self.report({'WARNING'}, "View3D not found, cannot run operator")
1023 return {'CANCELLED'}
1026 class NWDeleteUnused(Operator, NWBase):
1027 """Delete all nodes whose output is not used"""
1028 bl_idname = 'node.nw_del_unused'
1029 bl_label = 'Delete Unused Nodes'
1030 bl_options = {'REGISTER', 'UNDO'}
1032 delete_muted: BoolProperty(name="Delete Muted", description="Delete (but reconnect, like Ctrl-X) all muted nodes", default=True)
1033 delete_frames: BoolProperty(name="Delete Empty Frames", description="Delete all frames that have no nodes inside them", default=True)
1035 def is_unused_node(self, node):
1036 end_types = ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1037 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1038 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1039 if node.type in end_types:
1040 return False
1042 for output in node.outputs:
1043 if output.links:
1044 return False
1045 return True
1047 @classmethod
1048 def poll(cls, context):
1049 valid = False
1050 if nw_check(context):
1051 if context.space_data.node_tree.nodes:
1052 valid = True
1053 return valid
1055 def execute(self, context):
1056 nodes, links = get_nodes_links(context)
1058 # Store selection
1059 selection = []
1060 for node in nodes:
1061 if node.select == True:
1062 selection.append(node.name)
1064 for node in nodes:
1065 node.select = False
1067 deleted_nodes = []
1068 temp_deleted_nodes = []
1069 del_unused_iterations = len(nodes)
1070 for it in range(0, del_unused_iterations):
1071 temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
1072 for node in nodes:
1073 if self.is_unused_node(node):
1074 node.select = True
1075 deleted_nodes.append(node.name)
1076 bpy.ops.node.delete()
1078 if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
1079 break
1081 if self.delete_frames:
1082 repeat = True
1083 while repeat:
1084 frames_in_use = []
1085 frames = []
1086 repeat = False
1087 for node in nodes:
1088 if node.parent:
1089 frames_in_use.append(node.parent)
1090 for node in nodes:
1091 if node.type == 'FRAME' and node not in frames_in_use:
1092 frames.append(node)
1093 if node.parent:
1094 repeat = True # repeat for nested frames
1095 for node in frames:
1096 if node not in frames_in_use:
1097 node.select = True
1098 deleted_nodes.append(node.name)
1099 bpy.ops.node.delete()
1101 if self.delete_muted:
1102 for node in nodes:
1103 if node.mute:
1104 node.select = True
1105 deleted_nodes.append(node.name)
1106 bpy.ops.node.delete_reconnect()
1108 # get unique list of deleted nodes (iterations would count the same node more than once)
1109 deleted_nodes = list(set(deleted_nodes))
1110 for n in deleted_nodes:
1111 self.report({'INFO'}, "Node " + n + " deleted")
1112 num_deleted = len(deleted_nodes)
1113 n = ' node'
1114 if num_deleted > 1:
1115 n += 's'
1116 if num_deleted:
1117 self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
1118 else:
1119 self.report({'INFO'}, "Nothing deleted")
1121 # Restore selection
1122 nodes, links = get_nodes_links(context)
1123 for node in nodes:
1124 if node.name in selection:
1125 node.select = True
1126 return {'FINISHED'}
1128 def invoke(self, context, event):
1129 return context.window_manager.invoke_confirm(self, event)
1132 class NWSwapLinks(Operator, NWBase):
1133 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1134 bl_idname = 'node.nw_swap_links'
1135 bl_label = 'Swap Links'
1136 bl_options = {'REGISTER', 'UNDO'}
1138 @classmethod
1139 def poll(cls, context):
1140 valid = False
1141 if nw_check(context):
1142 if context.selected_nodes:
1143 valid = len(context.selected_nodes) <= 2
1144 return valid
1146 def execute(self, context):
1147 nodes, links = get_nodes_links(context)
1148 selected_nodes = context.selected_nodes
1149 n1 = selected_nodes[0]
1151 # Swap outputs
1152 if len(selected_nodes) == 2:
1153 n2 = selected_nodes[1]
1154 if n1.outputs and n2.outputs:
1155 n1_outputs = []
1156 n2_outputs = []
1158 out_index = 0
1159 for output in n1.outputs:
1160 if output.links:
1161 for link in output.links:
1162 n1_outputs.append([out_index, link.to_socket])
1163 links.remove(link)
1164 out_index += 1
1166 out_index = 0
1167 for output in n2.outputs:
1168 if output.links:
1169 for link in output.links:
1170 n2_outputs.append([out_index, link.to_socket])
1171 links.remove(link)
1172 out_index += 1
1174 for connection in n1_outputs:
1175 try:
1176 links.new(n2.outputs[connection[0]], connection[1])
1177 except:
1178 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1179 for connection in n2_outputs:
1180 try:
1181 links.new(n1.outputs[connection[0]], connection[1])
1182 except:
1183 self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1184 else:
1185 if n1.outputs or n2.outputs:
1186 self.report({'WARNING'}, "One of the nodes has no outputs!")
1187 else:
1188 self.report({'WARNING'}, "Neither of the nodes have outputs!")
1190 # Swap Inputs
1191 elif len(selected_nodes) == 1:
1192 if n1.inputs and n1.inputs[0].is_multi_input:
1193 self.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1194 return {'FINISHED'}
1195 if n1.inputs:
1196 types = []
1198 for i1 in n1.inputs:
1199 if i1.is_linked and not i1.is_multi_input:
1200 similar_types = 0
1201 for i2 in n1.inputs:
1202 if i1.type == i2.type and i2.is_linked and not i2.is_multi_input:
1203 similar_types += 1
1204 types.append ([i1, similar_types, i])
1205 i += 1
1206 types.sort(key=lambda k: k[1], reverse=True)
1208 if types:
1209 t = types[0]
1210 if t[1] == 2:
1211 for i2 in n1.inputs:
1212 if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
1213 pair = [t[0], i2]
1214 i1f = pair[0].links[0].from_socket
1215 i1t = pair[0].links[0].to_socket
1216 i2f = pair[1].links[0].from_socket
1217 i2t = pair[1].links[0].to_socket
1218 links.new(i1f, i2t)
1219 links.new(i2f, i1t)
1220 if t[1] == 1:
1221 if len(types) == 1:
1222 fs = t[0].links[0].from_socket
1223 i = t[2]
1224 links.remove(t[0].links[0])
1225 if i+1 == len(n1.inputs):
1226 i = -1
1227 i += 1
1228 while n1.inputs[i].is_linked:
1229 i += 1
1230 links.new(fs, n1.inputs[i])
1231 elif len(types) == 2:
1232 i1f = types[0][0].links[0].from_socket
1233 i1t = types[0][0].links[0].to_socket
1234 i2f = types[1][0].links[0].from_socket
1235 i2t = types[1][0].links[0].to_socket
1236 links.new(i1f, i2t)
1237 links.new(i2f, i1t)
1239 else:
1240 self.report({'WARNING'}, "This node has no input connections to swap!")
1241 else:
1242 self.report({'WARNING'}, "This node has no inputs to swap!")
1244 force_update(context)
1245 return {'FINISHED'}
1248 class NWResetBG(Operator, NWBase):
1249 """Reset the zoom and position of the background image"""
1250 bl_idname = 'node.nw_bg_reset'
1251 bl_label = 'Reset Backdrop'
1252 bl_options = {'REGISTER', 'UNDO'}
1254 @classmethod
1255 def poll(cls, context):
1256 valid = False
1257 if nw_check(context):
1258 snode = context.space_data
1259 valid = snode.tree_type == 'CompositorNodeTree'
1260 return valid
1262 def execute(self, context):
1263 context.space_data.backdrop_zoom = 1
1264 context.space_data.backdrop_offset[0] = 0
1265 context.space_data.backdrop_offset[1] = 0
1266 return {'FINISHED'}
1269 class NWAddAttrNode(Operator, NWBase):
1270 """Add an Attribute node with this name"""
1271 bl_idname = 'node.nw_add_attr_node'
1272 bl_label = 'Add UV map'
1273 bl_options = {'REGISTER', 'UNDO'}
1275 attr_name: StringProperty()
1277 def execute(self, context):
1278 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
1279 nodes, links = get_nodes_links(context)
1280 nodes.active.attribute_name = self.attr_name
1281 return {'FINISHED'}
1283 class NWPreviewNode(Operator, NWBase):
1284 bl_idname = "node.nw_preview_node"
1285 bl_label = "Preview Node"
1286 bl_description = "Connect active node to the Node Group output or the Material Output"
1287 bl_options = {'REGISTER', 'UNDO'}
1289 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1290 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1291 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1292 run_in_geometry_nodes: BoolProperty(default=True)
1294 def __init__(self):
1295 self.shader_output_type = ""
1296 self.shader_output_ident = ""
1298 @classmethod
1299 def poll(cls, context):
1300 if nw_check(context):
1301 space = context.space_data
1302 if space.tree_type == 'ShaderNodeTree' or space.tree_type == 'GeometryNodeTree':
1303 if context.active_node:
1304 if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
1305 return True
1306 else:
1307 return True
1308 return False
1310 def ensure_viewer_socket(self, node, socket_type, connect_socket=None):
1311 #check if a viewer output already exists in a node group otherwise create
1312 if hasattr(node, "node_tree"):
1313 index = None
1314 if len(node.node_tree.outputs):
1315 free_socket = None
1316 for i, socket in enumerate(node.node_tree.outputs):
1317 if is_viewer_socket(socket) and is_visible_socket(node.outputs[i]) and socket.type == socket_type:
1318 #if viewer output is already used but leads to the same socket we can still use it
1319 is_used = self.is_socket_used_other_mats(socket)
1320 if is_used:
1321 if connect_socket == None:
1322 continue
1323 groupout = get_group_output_node(node.node_tree)
1324 groupout_input = groupout.inputs[i]
1325 links = groupout_input.links
1326 if connect_socket not in [link.from_socket for link in links]:
1327 continue
1328 index=i
1329 break
1330 if not free_socket:
1331 free_socket = i
1332 if not index and free_socket:
1333 index = free_socket
1335 if not index:
1336 #create viewer socket
1337 node.node_tree.outputs.new(socket_type, viewer_socket_name)
1338 index = len(node.node_tree.outputs) - 1
1339 node.node_tree.outputs[index].NWViewerSocket = True
1340 return index
1342 def init_shader_variables(self, space, shader_type):
1343 if shader_type == 'OBJECT':
1344 if space.id not in [light for light in bpy.data.lights]: # cannot use bpy.data.lights directly as iterable
1345 self.shader_output_type = "OUTPUT_MATERIAL"
1346 self.shader_output_ident = "ShaderNodeOutputMaterial"
1347 else:
1348 self.shader_output_type = "OUTPUT_LIGHT"
1349 self.shader_output_ident = "ShaderNodeOutputLight"
1351 elif shader_type == 'WORLD':
1352 self.shader_output_type = "OUTPUT_WORLD"
1353 self.shader_output_ident = "ShaderNodeOutputWorld"
1355 def get_shader_output_node(self, tree):
1356 for node in tree.nodes:
1357 if node.type == self.shader_output_type and node.is_active_output == True:
1358 return node
1360 @classmethod
1361 def ensure_group_output(cls, tree):
1362 #check if a group output node exists otherwise create
1363 groupout = get_group_output_node(tree)
1364 if not groupout:
1365 groupout = tree.nodes.new('NodeGroupOutput')
1366 loc_x, loc_y = get_output_location(tree)
1367 groupout.location.x = loc_x
1368 groupout.location.y = loc_y
1369 groupout.select = False
1370 # So that we don't keep on adding new group outputs
1371 groupout.is_active_output = True
1372 return groupout
1374 @classmethod
1375 def search_sockets(cls, node, sockets, index=None):
1376 # recursively scan nodes for viewer sockets and store in list
1377 for i, input_socket in enumerate(node.inputs):
1378 if index and i != index:
1379 continue
1380 if len(input_socket.links):
1381 link = input_socket.links[0]
1382 next_node = link.from_node
1383 external_socket = link.from_socket
1384 if hasattr(next_node, "node_tree"):
1385 for socket_index, s in enumerate(next_node.outputs):
1386 if s == external_socket:
1387 break
1388 socket = next_node.node_tree.outputs[socket_index]
1389 if is_viewer_socket(socket) and socket not in sockets:
1390 sockets.append(socket)
1391 #continue search inside of node group but restrict socket to where we came from
1392 groupout = get_group_output_node(next_node.node_tree)
1393 cls.search_sockets(groupout, sockets, index=socket_index)
1395 @classmethod
1396 def scan_nodes(cls, tree, sockets):
1397 # get all viewer sockets in a material tree
1398 for node in tree.nodes:
1399 if hasattr(node, "node_tree"):
1400 for socket in node.node_tree.outputs:
1401 if is_viewer_socket(socket) and (socket not in sockets):
1402 sockets.append(socket)
1403 cls.scan_nodes(node.node_tree, sockets)
1405 def link_leads_to_used_socket(self, link):
1406 #return True if link leads to a socket that is already used in this material
1407 socket = get_internal_socket(link.to_socket)
1408 return (socket and self.is_socket_used_active_mat(socket))
1410 def is_socket_used_active_mat(self, socket):
1411 #ensure used sockets in active material is calculated and check given socket
1412 if not hasattr(self, "used_viewer_sockets_active_mat"):
1413 self.used_viewer_sockets_active_mat = []
1414 materialout = self.get_shader_output_node(bpy.context.space_data.node_tree)
1415 if materialout:
1416 self.search_sockets(materialout, self.used_viewer_sockets_active_mat)
1417 return socket in self.used_viewer_sockets_active_mat
1419 def is_socket_used_other_mats(self, socket):
1420 #ensure used sockets in other materials are calculated and check given socket
1421 if not hasattr(self, "used_viewer_sockets_other_mats"):
1422 self.used_viewer_sockets_other_mats = []
1423 for mat in bpy.data.materials:
1424 if mat.node_tree == bpy.context.space_data.node_tree or not hasattr(mat.node_tree, "nodes"):
1425 continue
1426 # get viewer node
1427 materialout = self.get_shader_output_node(mat.node_tree)
1428 if materialout:
1429 self.search_sockets(materialout, self.used_viewer_sockets_other_mats)
1430 return socket in self.used_viewer_sockets_other_mats
1432 def invoke(self, context, event):
1433 space = context.space_data
1434 # Ignore operator when running in wrong context.
1435 if self.run_in_geometry_nodes != (space.tree_type == "GeometryNodeTree"):
1436 return {'PASS_THROUGH'}
1438 shader_type = space.shader_type
1439 self.init_shader_variables(space, shader_type)
1440 mlocx = event.mouse_region_x
1441 mlocy = event.mouse_region_y
1442 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
1443 if 'FINISHED' in select_node: # only run if mouse click is on a node
1444 active_tree, path_to_tree = get_active_tree(context)
1445 nodes, links = active_tree.nodes, active_tree.links
1446 base_node_tree = space.node_tree
1447 active = nodes.active
1449 # For geometry node trees we just connect to the group output
1450 if space.tree_type == "GeometryNodeTree":
1451 valid = False
1452 if active:
1453 for out in active.outputs:
1454 if is_visible_socket(out):
1455 valid = True
1456 break
1457 # Exit early
1458 if not valid:
1459 return {'FINISHED'}
1461 delete_sockets = []
1463 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1464 self.scan_nodes(base_node_tree, delete_sockets)
1466 # Find (or create if needed) the output of this node tree
1467 geometryoutput = self.ensure_group_output(base_node_tree)
1469 # Analyze outputs, make links
1470 out_i = None
1471 valid_outputs = []
1472 for i, out in enumerate(active.outputs):
1473 if is_visible_socket(out) and out.type == 'GEOMETRY':
1474 valid_outputs.append(i)
1475 if valid_outputs:
1476 out_i = valid_outputs[0] # Start index of node's outputs
1477 for i, valid_i in enumerate(valid_outputs):
1478 for out_link in active.outputs[valid_i].links:
1479 if is_viewer_link(out_link, geometryoutput):
1480 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1481 if i < len(valid_outputs) - 1:
1482 out_i = valid_outputs[i + 1]
1483 else:
1484 out_i = valid_outputs[0]
1486 make_links = [] # store sockets for new links
1487 if active.outputs:
1488 # If there is no 'GEOMETRY' output type - We can't preview the node
1489 if out_i is None:
1490 return {'FINISHED'}
1491 socket_type = 'GEOMETRY'
1492 # Find an input socket of the output of type geometry
1493 geometryoutindex = None
1494 for i,inp in enumerate(geometryoutput.inputs):
1495 if inp.type == socket_type:
1496 geometryoutindex = i
1497 break
1498 if geometryoutindex is None:
1499 # Create geometry socket
1500 geometryoutput.inputs.new(socket_type, 'Geometry')
1501 geometryoutindex = len(geometryoutput.inputs) - 1
1503 make_links.append((active.outputs[out_i], geometryoutput.inputs[geometryoutindex]))
1504 output_socket = geometryoutput.inputs[geometryoutindex]
1505 for li_from, li_to in make_links:
1506 base_node_tree.links.new(li_from, li_to)
1507 tree = base_node_tree
1508 link_end = output_socket
1509 while tree.nodes.active != active:
1510 node = tree.nodes.active
1511 index = self.ensure_viewer_socket(node,'NodeSocketGeometry', connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1512 link_start = node.outputs[index]
1513 node_socket = node.node_tree.outputs[index]
1514 if node_socket in delete_sockets:
1515 delete_sockets.remove(node_socket)
1516 tree.links.new(link_start, link_end)
1517 # Iterate
1518 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1519 tree = tree.nodes.active.node_tree
1520 tree.links.new(active.outputs[out_i], link_end)
1522 # Delete sockets
1523 for socket in delete_sockets:
1524 tree = socket.id_data
1525 tree.outputs.remove(socket)
1527 nodes.active = active
1528 active.select = True
1529 force_update(context)
1530 return {'FINISHED'}
1533 # What follows is code for the shader editor
1534 output_types = [x.nodetype for x in
1535 get_nodes_from_category('Output', context)]
1536 valid = False
1537 if active:
1538 if active.rna_type.identifier not in output_types:
1539 for out in active.outputs:
1540 if is_visible_socket(out):
1541 valid = True
1542 break
1543 if valid:
1544 # get material_output node
1545 materialout = None # placeholder node
1546 delete_sockets = []
1548 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1549 self.scan_nodes(base_node_tree, delete_sockets)
1551 materialout = self.get_shader_output_node(base_node_tree)
1552 if not materialout:
1553 materialout = base_node_tree.nodes.new(self.shader_output_ident)
1554 materialout.location = get_output_location(base_node_tree)
1555 materialout.select = False
1556 # Analyze outputs
1557 out_i = None
1558 valid_outputs = []
1559 for i, out in enumerate(active.outputs):
1560 if is_visible_socket(out):
1561 valid_outputs.append(i)
1562 if valid_outputs:
1563 out_i = valid_outputs[0] # Start index of node's outputs
1564 for i, valid_i in enumerate(valid_outputs):
1565 for out_link in active.outputs[valid_i].links:
1566 if is_viewer_link(out_link, materialout):
1567 if nodes == base_node_tree.nodes or self.link_leads_to_used_socket(out_link):
1568 if i < len(valid_outputs) - 1:
1569 out_i = valid_outputs[i + 1]
1570 else:
1571 out_i = valid_outputs[0]
1573 make_links = [] # store sockets for new links
1574 if active.outputs:
1575 socket_type = 'NodeSocketShader'
1576 materialout_index = 1 if active.outputs[out_i].name == "Volume" else 0
1577 make_links.append((active.outputs[out_i], materialout.inputs[materialout_index]))
1578 output_socket = materialout.inputs[materialout_index]
1579 for li_from, li_to in make_links:
1580 base_node_tree.links.new(li_from, li_to)
1582 # Create links through node groups until we reach the active node
1583 tree = base_node_tree
1584 link_end = output_socket
1585 while tree.nodes.active != active:
1586 node = tree.nodes.active
1587 index = self.ensure_viewer_socket(node, socket_type, connect_socket=active.outputs[out_i] if node.node_tree.nodes.active == active else None)
1588 link_start = node.outputs[index]
1589 node_socket = node.node_tree.outputs[index]
1590 if node_socket in delete_sockets:
1591 delete_sockets.remove(node_socket)
1592 tree.links.new(link_start, link_end)
1593 # Iterate
1594 link_end = self.ensure_group_output(node.node_tree).inputs[index]
1595 tree = tree.nodes.active.node_tree
1596 tree.links.new(active.outputs[out_i], link_end)
1598 # Delete sockets
1599 for socket in delete_sockets:
1600 if not self.is_socket_used_other_mats(socket):
1601 tree = socket.id_data
1602 tree.outputs.remove(socket)
1604 nodes.active = active
1605 active.select = True
1607 force_update(context)
1609 return {'FINISHED'}
1610 else:
1611 return {'CANCELLED'}
1614 class NWFrameSelected(Operator, NWBase):
1615 bl_idname = "node.nw_frame_selected"
1616 bl_label = "Frame Selected"
1617 bl_description = "Add a frame node and parent the selected nodes to it"
1618 bl_options = {'REGISTER', 'UNDO'}
1620 label_prop: StringProperty(
1621 name='Label',
1622 description='The visual name of the frame node',
1623 default=' '
1625 use_custom_color_prop: BoolProperty(
1626 name="Custom Color",
1627 description="Use custom color for the frame node",
1628 default=False
1630 color_prop: FloatVectorProperty(
1631 name="Color",
1632 description="The color of the frame node",
1633 default=(0.604, 0.604, 0.604),
1634 min=0, max=1, step=1, precision=3,
1635 subtype='COLOR_GAMMA', size=3
1638 def draw(self, context):
1639 layout = self.layout
1640 layout.prop(self, 'label_prop')
1641 layout.prop(self, 'use_custom_color_prop')
1642 col = layout.column()
1643 col.active = self.use_custom_color_prop
1644 col.prop(self, 'color_prop', text="")
1646 def execute(self, context):
1647 nodes, links = get_nodes_links(context)
1648 selected = []
1649 for node in nodes:
1650 if node.select == True:
1651 selected.append(node)
1653 bpy.ops.node.add_node(type='NodeFrame')
1654 frm = nodes.active
1655 frm.label = self.label_prop
1656 frm.use_custom_color = self.use_custom_color_prop
1657 frm.color = self.color_prop
1659 for node in selected:
1660 node.parent = frm
1662 return {'FINISHED'}
1665 class NWReloadImages(Operator):
1666 bl_idname = "node.nw_reload_images"
1667 bl_label = "Reload Images"
1668 bl_description = "Update all the image nodes to match their files on disk"
1670 @classmethod
1671 def poll(cls, context):
1672 valid = False
1673 if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
1674 if context.active_node is not None:
1675 for out in context.active_node.outputs:
1676 if is_visible_socket(out):
1677 valid = True
1678 break
1679 return valid
1681 def execute(self, context):
1682 nodes, links = get_nodes_links(context)
1683 image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1684 num_reloaded = 0
1685 for node in nodes:
1686 if node.type in image_types:
1687 if node.type == "TEXTURE":
1688 if node.texture: # node has texture assigned
1689 if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1690 if node.texture.image: # texture has image assigned
1691 node.texture.image.reload()
1692 num_reloaded += 1
1693 else:
1694 if node.image:
1695 node.image.reload()
1696 num_reloaded += 1
1698 if num_reloaded:
1699 self.report({'INFO'}, "Reloaded images")
1700 print("Reloaded " + str(num_reloaded) + " images")
1701 force_update(context)
1702 return {'FINISHED'}
1703 else:
1704 self.report({'WARNING'}, "No images found to reload in this node tree")
1705 return {'CANCELLED'}
1708 class NWSwitchNodeType(Operator, NWBase):
1709 """Switch type of selected nodes """
1710 bl_idname = "node.nw_swtch_node_type"
1711 bl_label = "Switch Node Type"
1712 bl_options = {'REGISTER', 'UNDO'}
1714 to_type: StringProperty(
1715 name="Switch to type",
1716 default = '',
1719 def execute(self, context):
1720 to_type = self.to_type
1721 if len(to_type) == 0:
1722 return {'CANCELLED'}
1724 nodes, links = get_nodes_links(context)
1725 # Those types of nodes will not swap.
1726 src_excludes = ('NodeFrame')
1727 # Those attributes of nodes will be copied if possible
1728 attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
1729 'show_options', 'show_preview', 'show_texture',
1730 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1732 selected = [n for n in nodes if n.select]
1733 reselect = []
1734 for node in [n for n in selected if
1735 n.rna_type.identifier not in src_excludes and
1736 n.rna_type.identifier != to_type]:
1737 new_node = nodes.new(to_type)
1738 for attr in attrs_to_pass:
1739 if hasattr(node, attr) and hasattr(new_node, attr):
1740 setattr(new_node, attr, getattr(node, attr))
1741 # set image datablock of dst to image of src
1742 if hasattr(node, 'image') and hasattr(new_node, 'image'):
1743 if node.image:
1744 new_node.image = node.image
1745 # Special cases
1746 if new_node.type == 'SWITCH':
1747 new_node.hide = True
1748 # Dictionaries: src_sockets and dst_sockets:
1749 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1750 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1751 # in 'INPUTS' and 'OUTPUTS':
1752 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1753 # socket entry:
1754 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1755 src_sockets = {
1756 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1757 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1759 dst_sockets = {
1760 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1761 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1763 types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1764 types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1765 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1766 for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
1767 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1768 for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
1769 # enumerate in inputs, then in outputs
1770 # find name, default value and links of socket
1771 for i, socket in enumerate(in_out):
1772 the_name = socket.name
1773 dval = None
1774 # Not every socket, especially in outputs has "default_value"
1775 if hasattr(socket, 'default_value'):
1776 dval = socket.default_value
1777 socket_links = []
1778 for lnk in socket.links:
1779 socket_links.append(lnk)
1780 # check type of socket to fill proper keys.
1781 for the_type in types_order_one:
1782 if socket.type == the_type:
1783 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1784 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1785 sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
1786 # Check which of the types in inputs/outputs is considered to be "main".
1787 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1788 for type_check in types_order_one:
1789 if sockets[in_out_name][type_check]:
1790 sockets[in_out_name]['MAIN'] = type_check
1791 break
1793 matches = {
1794 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1795 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1798 for inout, soctype in (
1799 ('INPUTS', 'MAIN',),
1800 ('INPUTS', 'SHADER',),
1801 ('INPUTS', 'RGBA',),
1802 ('INPUTS', 'VECTOR',),
1803 ('INPUTS', 'VALUE',),
1804 ('OUTPUTS', 'MAIN',),
1805 ('OUTPUTS', 'SHADER',),
1806 ('OUTPUTS', 'RGBA',),
1807 ('OUTPUTS', 'VECTOR',),
1808 ('OUTPUTS', 'VALUE',),
1810 if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
1811 if soctype == 'MAIN':
1812 sc = src_sockets[inout][src_sockets[inout]['MAIN']]
1813 dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
1814 else:
1815 sc = src_sockets[inout][soctype]
1816 dt = dst_sockets[inout][soctype]
1817 # start with 'dt' to determine number of possibilities.
1818 for i, soc in enumerate(dt):
1819 # if src main has enough entries - match them with dst main sockets by indexes.
1820 if len(sc) > i:
1821 matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
1822 # add 'VALUE_NAME' criterion to inputs.
1823 if inout == 'INPUTS' and soctype == 'VALUE':
1824 for s in sc:
1825 if s[2] == soc[2]: # if names match
1826 # append src (index, dval), dst (index, dval)
1827 matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
1829 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1830 # This creates better links when relinking textures.
1831 if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
1832 matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
1834 # Pass default values and RELINK:
1835 for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1836 # INPUTS: Base on matches in proper order.
1837 for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
1838 # pass dvals
1839 if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
1840 new_node.inputs[dst_i].default_value = src_dval
1841 # Special case: switch to math
1842 if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1843 new_node.type == 'MATH' and\
1844 tp == 'MAIN':
1845 new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
1846 new_node.inputs[dst_i].default_value = new_dst_dval
1847 if node.type == 'MIX_RGB':
1848 if node.blend_type in [o[0] for o in operations]:
1849 new_node.operation = node.blend_type
1850 # Special case: switch from math to some types
1851 if node.type == 'MATH' and\
1852 new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1853 tp == 'MAIN':
1854 for i in range(3):
1855 new_node.inputs[dst_i].default_value[i] = src_dval
1856 if new_node.type == 'MIX_RGB':
1857 if node.operation in [t[0] for t in blend_types]:
1858 new_node.blend_type = node.operation
1859 # Set Fac of MIX_RGB to 1.0
1860 new_node.inputs[0].default_value = 1.0
1861 # make link only when dst matching input is not linked already.
1862 if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
1863 in_src_link = node.inputs[src_i].links[0]
1864 in_dst_socket = new_node.inputs[dst_i]
1865 links.new(in_src_link.from_socket, in_dst_socket)
1866 links.remove(in_src_link)
1867 # OUTPUTS: Base on matches in proper order.
1868 for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
1869 for out_src_link in node.outputs[src_i].links:
1870 out_dst_socket = new_node.outputs[dst_i]
1871 links.new(out_dst_socket, out_src_link.to_socket)
1872 # relink rest inputs if possible, no criteria
1873 for src_inp in node.inputs:
1874 for dst_inp in new_node.inputs:
1875 if src_inp.links and not dst_inp.links:
1876 src_link = src_inp.links[0]
1877 links.new(src_link.from_socket, dst_inp)
1878 links.remove(src_link)
1879 # relink rest outputs if possible, base on node kind if any left.
1880 for src_o in node.outputs:
1881 for out_src_link in src_o.links:
1882 for dst_o in new_node.outputs:
1883 if src_o.type == dst_o.type:
1884 links.new(dst_o, out_src_link.to_socket)
1885 # relink rest outputs no criteria if any left. Link all from first output.
1886 for src_o in node.outputs:
1887 for out_src_link in src_o.links:
1888 if new_node.outputs:
1889 links.new(new_node.outputs[0], out_src_link.to_socket)
1890 nodes.remove(node)
1891 force_update(context)
1892 return {'FINISHED'}
1895 class NWMergeNodes(Operator, NWBase):
1896 bl_idname = "node.nw_merge_nodes"
1897 bl_label = "Merge Nodes"
1898 bl_description = "Merge Selected Nodes"
1899 bl_options = {'REGISTER', 'UNDO'}
1901 mode: EnumProperty(
1902 name="mode",
1903 description="All possible blend types, boolean operations and math operations",
1904 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],
1906 merge_type: EnumProperty(
1907 name="merge type",
1908 description="Type of Merge to be used",
1909 items=(
1910 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1911 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1912 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
1913 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1914 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1915 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
1916 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
1920 # Check if the link connects to a node that is in selected_nodes
1921 # If not, then check recursively for each link in the nodes outputs.
1922 # If yes, return True. If the recursion stops without finding a node
1923 # in selected_nodes, it returns False. The depth is used to prevent
1924 # getting stuck in a loop because of an already present cycle.
1925 @staticmethod
1926 def link_creates_cycle(link, selected_nodes, depth=0)->bool:
1927 if depth > 255:
1928 # We're stuck in a cycle, but that cycle was already present,
1929 # so we return False.
1930 # NOTE: The number 255 is arbitrary, but seems to work well.
1931 return False
1932 node = link.to_node
1933 if node in selected_nodes:
1934 return True
1935 if not node.outputs:
1936 return False
1937 for output in node.outputs:
1938 if output.is_linked:
1939 for olink in output.links:
1940 if NWMergeNodes.link_creates_cycle(olink, selected_nodes, depth+1):
1941 return True
1942 # None of the outputs found a node in selected_nodes, so there is no cycle.
1943 return False
1945 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
1946 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
1947 # be connected. The last one is assumed to be a multi input socket.
1948 # For convenience the node is returned.
1949 @staticmethod
1950 def merge_with_multi_input(nodes_list, merge_position,do_hide, loc_x, links, nodes, node_name, socket_indices):
1951 # The y-location of the last node
1952 loc_y = nodes_list[-1][2]
1953 if merge_position == 'CENTER':
1954 # Average the y-location
1955 for i in range(len(nodes_list)-1):
1956 loc_y += nodes_list[i][2]
1957 loc_y = loc_y/len(nodes_list)
1958 new_node = nodes.new(node_name)
1959 new_node.hide = do_hide
1960 new_node.location.x = loc_x
1961 new_node.location.y = loc_y
1962 selected_nodes = [nodes[node_info[0]] for node_info in nodes_list]
1963 prev_links = []
1964 outputs_for_multi_input = []
1965 for i,node in enumerate(selected_nodes):
1966 node.select = False
1967 # Search for the first node which had output links that do not create
1968 # a cycle, which we can then reconnect afterwards.
1969 if prev_links == [] and node.outputs[0].is_linked:
1970 prev_links = [link for link in node.outputs[0].links if not NWMergeNodes.link_creates_cycle(link, selected_nodes)]
1971 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
1972 # To get the placement to look right we need to reverse the order in which we connect the
1973 # outputs to the multi input socket.
1974 if i < len(socket_indices) - 1:
1975 ind = socket_indices[i]
1976 links.new(node.outputs[0], new_node.inputs[ind])
1977 else:
1978 outputs_for_multi_input.insert(0, node.outputs[0])
1979 if outputs_for_multi_input != []:
1980 ind = socket_indices[-1]
1981 for output in outputs_for_multi_input:
1982 links.new(output, new_node.inputs[ind])
1983 if prev_links != []:
1984 for link in prev_links:
1985 links.new(new_node.outputs[0], link.to_node.inputs[0])
1986 return new_node
1988 def execute(self, context):
1989 settings = context.preferences.addons[__name__].preferences
1990 merge_hide = settings.merge_hide
1991 merge_position = settings.merge_position # 'center' or 'bottom'
1993 do_hide = False
1994 do_hide_shader = False
1995 if merge_hide == 'ALWAYS':
1996 do_hide = True
1997 do_hide_shader = True
1998 elif merge_hide == 'NON_SHADER':
1999 do_hide = True
2001 tree_type = context.space_data.node_tree.type
2002 if tree_type == 'GEOMETRY':
2003 node_type = 'GeometryNode'
2004 if tree_type == 'COMPOSITING':
2005 node_type = 'CompositorNode'
2006 elif tree_type == 'SHADER':
2007 node_type = 'ShaderNode'
2008 elif tree_type == 'TEXTURE':
2009 node_type = 'TextureNode'
2010 nodes, links = get_nodes_links(context)
2011 mode = self.mode
2012 merge_type = self.merge_type
2013 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2014 # 'ZCOMBINE' works only if mode == 'MIX'
2015 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2016 if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
2017 merge_type = 'MIX'
2018 mode = 'MIX'
2019 if (merge_type != 'MATH' and merge_type != 'GEOMETRY') and tree_type == 'GEOMETRY':
2020 merge_type = 'AUTO'
2021 # The MixRGB node and math nodes used for geometry nodes are of type 'ShaderNode'
2022 if (merge_type == 'MATH' or merge_type == 'MIX') and tree_type == 'GEOMETRY':
2023 node_type = 'ShaderNode'
2024 selected_mix = [] # entry = [index, loc]
2025 selected_shader = [] # entry = [index, loc]
2026 selected_geometry = [] # entry = [index, loc]
2027 selected_math = [] # entry = [index, loc]
2028 selected_vector = [] # entry = [index, loc]
2029 selected_z = [] # entry = [index, loc]
2030 selected_alphaover = [] # entry = [index, loc]
2032 for i, node in enumerate(nodes):
2033 if node.select and node.outputs:
2034 if merge_type == 'AUTO':
2035 for (type, types_list, dst) in (
2036 ('SHADER', ('MIX', 'ADD'), selected_shader),
2037 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2038 ('RGBA', [t[0] for t in blend_types], selected_mix),
2039 ('VALUE', [t[0] for t in operations], selected_math),
2040 ('VECTOR', [], selected_vector),
2042 output = get_first_enabled_output(node)
2043 output_type = output.type
2044 valid_mode = mode in types_list
2045 # When mode is 'MIX' we have to cheat since the mix node is not used in
2046 # geometry nodes.
2047 if tree_type == 'GEOMETRY':
2048 if mode == 'MIX':
2049 if output_type == 'VALUE' and type == 'VALUE':
2050 valid_mode = True
2051 elif output_type == 'VECTOR' and type == 'VECTOR':
2052 valid_mode = True
2053 elif type == 'GEOMETRY':
2054 valid_mode = True
2055 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2056 # Cheat that output type is 'RGBA',
2057 # and that 'MIX' exists in math operations list.
2058 # This way when selected_mix list is analyzed:
2059 # Node data will be appended even though it doesn't meet requirements.
2060 elif output_type != 'SHADER' and mode == 'MIX':
2061 output_type = 'RGBA'
2062 valid_mode = True
2063 if output_type == type and valid_mode:
2064 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2065 else:
2066 for (type, types_list, dst) in (
2067 ('SHADER', ('MIX', 'ADD'), selected_shader),
2068 ('GEOMETRY', [t[0] for t in geo_combine_operations], selected_geometry),
2069 ('MIX', [t[0] for t in blend_types], selected_mix),
2070 ('MATH', [t[0] for t in operations], selected_math),
2071 ('ZCOMBINE', ('MIX', ), selected_z),
2072 ('ALPHAOVER', ('MIX', ), selected_alphaover),
2074 if merge_type == type and mode in types_list:
2075 dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
2076 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2077 # use only 'Mix' nodes for merging.
2078 # For that we add selected_math list to selected_mix list and clear selected_math.
2079 if selected_mix and selected_math and merge_type == 'AUTO':
2080 selected_mix += selected_math
2081 selected_math = []
2082 for nodes_list in [selected_mix, selected_shader, selected_geometry, selected_math, selected_vector, selected_z, selected_alphaover]:
2083 if not nodes_list:
2084 continue
2085 count_before = len(nodes)
2086 # sort list by loc_x - reversed
2087 nodes_list.sort(key=lambda k: k[1], reverse=True)
2088 # get maximum loc_x
2089 loc_x = nodes_list[0][1] + nodes_list[0][3] + 70
2090 nodes_list.sort(key=lambda k: k[2], reverse=True)
2092 # Change the node type for math nodes in a geometry node tree.
2093 if tree_type == 'GEOMETRY':
2094 if nodes_list is selected_math or nodes_list is selected_vector or nodes_list is selected_mix:
2095 node_type = 'ShaderNode'
2096 if mode == 'MIX':
2097 mode = 'ADD'
2098 else:
2099 node_type = 'GeometryNode'
2100 if merge_position == 'CENTER':
2101 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)
2102 if nodes_list[len(nodes_list) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2103 if do_hide:
2104 loc_y += 40
2105 else:
2106 loc_y += 80
2107 else:
2108 loc_y = nodes_list[len(nodes_list) - 1][2]
2109 offset_y = 100
2110 if not do_hide:
2111 offset_y = 200
2112 if nodes_list == selected_shader and not do_hide_shader:
2113 offset_y = 150.0
2114 the_range = len(nodes_list) - 1
2115 if len(nodes_list) == 1:
2116 the_range = 1
2117 was_multi = False
2118 for i in range(the_range):
2119 if nodes_list == selected_mix:
2120 add_type = node_type + 'MixRGB'
2121 add = nodes.new(add_type)
2122 add.blend_type = mode
2123 if mode != 'MIX':
2124 add.inputs[0].default_value = 1.0
2125 add.show_preview = False
2126 add.hide = do_hide
2127 if do_hide:
2128 loc_y = loc_y - 50
2129 first = 1
2130 second = 2
2131 add.width_hidden = 100.0
2132 elif nodes_list == selected_math:
2133 add_type = node_type + 'Math'
2134 add = nodes.new(add_type)
2135 add.operation = mode
2136 add.hide = do_hide
2137 if do_hide:
2138 loc_y = loc_y - 50
2139 first = 0
2140 second = 1
2141 add.width_hidden = 100.0
2142 elif nodes_list == selected_shader:
2143 if mode == 'MIX':
2144 add_type = node_type + 'MixShader'
2145 add = nodes.new(add_type)
2146 add.hide = do_hide_shader
2147 if do_hide_shader:
2148 loc_y = loc_y - 50
2149 first = 1
2150 second = 2
2151 add.width_hidden = 100.0
2152 elif mode == 'ADD':
2153 add_type = node_type + 'AddShader'
2154 add = nodes.new(add_type)
2155 add.hide = do_hide_shader
2156 if do_hide_shader:
2157 loc_y = loc_y - 50
2158 first = 0
2159 second = 1
2160 add.width_hidden = 100.0
2161 elif nodes_list == selected_geometry:
2162 if mode in ('JOIN', 'MIX'):
2163 add_type = node_type + 'JoinGeometry'
2164 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,[0])
2165 else:
2166 add_type = node_type + 'Boolean'
2167 indices = [0,1] if mode == 'DIFFERENCE' else [1]
2168 add = self.merge_with_multi_input(nodes_list, merge_position, do_hide, loc_x, links, nodes, add_type,indices)
2169 add.operation = mode
2170 was_multi = True
2171 break
2172 elif nodes_list == selected_vector:
2173 add_type = node_type + 'VectorMath'
2174 add = nodes.new(add_type)
2175 add.operation = mode
2176 add.hide = do_hide
2177 if do_hide:
2178 loc_y = loc_y - 50
2179 first = 0
2180 second = 1
2181 add.width_hidden = 100.0
2182 elif nodes_list == selected_z:
2183 add = nodes.new('CompositorNodeZcombine')
2184 add.show_preview = False
2185 add.hide = do_hide
2186 if do_hide:
2187 loc_y = loc_y - 50
2188 first = 0
2189 second = 2
2190 add.width_hidden = 100.0
2191 elif nodes_list == selected_alphaover:
2192 add = nodes.new('CompositorNodeAlphaOver')
2193 add.show_preview = False
2194 add.hide = do_hide
2195 if do_hide:
2196 loc_y = loc_y - 50
2197 first = 1
2198 second = 2
2199 add.width_hidden = 100.0
2200 add.location = loc_x, loc_y
2201 loc_y += offset_y
2202 add.select = True
2204 # This has already been handled separately
2205 if was_multi:
2206 continue
2207 count_adds = i + 1
2208 count_after = len(nodes)
2209 index = count_after - 1
2210 first_selected = nodes[nodes_list[0][0]]
2211 # "last" node has been added as first, so its index is count_before.
2212 last_add = nodes[count_before]
2213 # Create list of invalid indexes.
2214 invalid_nodes = [nodes[n[0]] for n in (selected_mix + selected_math + selected_shader + selected_z + selected_geometry)]
2216 # Special case:
2217 # Two nodes were selected and first selected has no output links, second selected has output links.
2218 # Then add links from last add to all links 'to_socket' of out links of second selected.
2219 first_selected_output = get_first_enabled_output(first_selected)
2220 if len(nodes_list) == 2:
2221 if not first_selected_output.links:
2222 second_selected = nodes[nodes_list[1][0]]
2223 for ss_link in second_selected.outputs[0].links:
2224 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2225 # Link only if "to_node" index not in invalid indexes list.
2226 if not self.link_creates_cycle(ss_link, invalid_nodes):
2227 links.new(last_add.outputs[0], ss_link.to_socket)
2228 # add links from last_add to all links 'to_socket' of out links of first selected.
2229 for fs_link in first_selected_output.links:
2230 # Link only if "to_node" index not in invalid indexes list.
2231 if not self.link_creates_cycle(fs_link, invalid_nodes):
2232 links.new(last_add.outputs[0], fs_link.to_socket)
2233 # add link from "first" selected and "first" add node
2234 node_to = nodes[count_after - 1]
2235 links.new(first_selected_output, node_to.inputs[first])
2236 if node_to.type == 'ZCOMBINE':
2237 for fs_out in first_selected.outputs:
2238 if fs_out != first_selected_output and fs_out.name in ('Z', 'Depth'):
2239 links.new(fs_out, node_to.inputs[1])
2240 break
2241 # add links between added ADD nodes and between selected and ADD nodes
2242 for i in range(count_adds):
2243 if i < count_adds - 1:
2244 node_from = nodes[index]
2245 node_to = nodes[index - 1]
2246 node_to_input_i = first
2247 node_to_z_i = 1 # if z combine - link z to first z input
2248 links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
2249 if node_to.type == 'ZCOMBINE':
2250 for from_out in node_from.outputs:
2251 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
2252 links.new(from_out, node_to.inputs[node_to_z_i])
2253 if len(nodes_list) > 1:
2254 node_from = nodes[nodes_list[i + 1][0]]
2255 node_to = nodes[index]
2256 node_to_input_i = second
2257 node_to_z_i = 3 # if z combine - link z to second z input
2258 links.new(get_first_enabled_output(node_from), node_to.inputs[node_to_input_i])
2259 if node_to.type == 'ZCOMBINE':
2260 for from_out in node_from.outputs:
2261 if from_out != get_first_enabled_output(node_from) and from_out.name in ('Z', 'Depth'):
2262 links.new(from_out, node_to.inputs[node_to_z_i])
2263 index -= 1
2264 # set "last" of added nodes as active
2265 nodes.active = last_add
2266 for i, x, y, dx, h in nodes_list:
2267 nodes[i].select = False
2269 return {'FINISHED'}
2272 class NWBatchChangeNodes(Operator, NWBase):
2273 bl_idname = "node.nw_batch_change"
2274 bl_label = "Batch Change"
2275 bl_description = "Batch Change Blend Type and Math Operation"
2276 bl_options = {'REGISTER', 'UNDO'}
2278 blend_type: EnumProperty(
2279 name="Blend Type",
2280 items=blend_types + navs,
2282 operation: EnumProperty(
2283 name="Operation",
2284 items=operations + navs,
2287 def execute(self, context):
2288 blend_type = self.blend_type
2289 operation = self.operation
2290 for node in context.selected_nodes:
2291 if node.type == 'MIX_RGB' or node.bl_idname == 'GeometryNodeAttributeMix':
2292 if not blend_type in [nav[0] for nav in navs]:
2293 node.blend_type = blend_type
2294 else:
2295 if blend_type == 'NEXT':
2296 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2297 #index = blend_types.index(node.blend_type)
2298 if index == len(blend_types) - 1:
2299 node.blend_type = blend_types[0][0]
2300 else:
2301 node.blend_type = blend_types[index + 1][0]
2303 if blend_type == 'PREV':
2304 index = [i for i, entry in enumerate(blend_types) if node.blend_type in entry][0]
2305 if index == 0:
2306 node.blend_type = blend_types[len(blend_types) - 1][0]
2307 else:
2308 node.blend_type = blend_types[index - 1][0]
2310 if node.type == 'MATH' or node.bl_idname == 'GeometryNodeAttributeMath':
2311 if not operation in [nav[0] for nav in navs]:
2312 node.operation = operation
2313 else:
2314 if operation == 'NEXT':
2315 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2316 #index = operations.index(node.operation)
2317 if index == len(operations) - 1:
2318 node.operation = operations[0][0]
2319 else:
2320 node.operation = operations[index + 1][0]
2322 if operation == 'PREV':
2323 index = [i for i, entry in enumerate(operations) if node.operation in entry][0]
2324 #index = operations.index(node.operation)
2325 if index == 0:
2326 node.operation = operations[len(operations) - 1][0]
2327 else:
2328 node.operation = operations[index - 1][0]
2330 return {'FINISHED'}
2333 class NWChangeMixFactor(Operator, NWBase):
2334 bl_idname = "node.nw_factor"
2335 bl_label = "Change Factor"
2336 bl_description = "Change Factors of Mix Nodes and Mix Shader Nodes"
2337 bl_options = {'REGISTER', 'UNDO'}
2339 # option: Change factor.
2340 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2341 # Else - change factor by option value.
2342 option: FloatProperty()
2344 def execute(self, context):
2345 nodes, links = get_nodes_links(context)
2346 option = self.option
2347 selected = [] # entry = index
2348 for si, node in enumerate(nodes):
2349 if node.select:
2350 if node.type in {'MIX_RGB', 'MIX_SHADER'}:
2351 selected.append(si)
2353 for si in selected:
2354 fac = nodes[si].inputs[0]
2355 nodes[si].hide = False
2356 if option in {0.0, 1.0}:
2357 fac.default_value = option
2358 else:
2359 fac.default_value += option
2361 return {'FINISHED'}
2364 class NWCopySettings(Operator, NWBase):
2365 bl_idname = "node.nw_copy_settings"
2366 bl_label = "Copy Settings"
2367 bl_description = "Copy Settings of Active Node to Selected Nodes"
2368 bl_options = {'REGISTER', 'UNDO'}
2370 @classmethod
2371 def poll(cls, context):
2372 valid = False
2373 if nw_check(context):
2374 if (
2375 context.active_node is not None and
2376 context.active_node.type != 'FRAME'
2378 valid = True
2379 return valid
2381 def execute(self, context):
2382 node_active = context.active_node
2383 node_selected = context.selected_nodes
2385 # Error handling
2386 if not (len(node_selected) > 1):
2387 self.report({'ERROR'}, "2 nodes must be selected at least")
2388 return {'CANCELLED'}
2390 # Check if active node is in the selection
2391 selected_node_names = [n.name for n in node_selected]
2392 if node_active.name not in selected_node_names:
2393 self.report({'ERROR'}, "No active node")
2394 return {'CANCELLED'}
2396 # Get nodes in selection by type
2397 valid_nodes = [n for n in node_selected if n.type == node_active.type]
2399 if not (len(valid_nodes) > 1) and node_active:
2400 self.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active.name))
2401 return {'CANCELLED'}
2403 if len(valid_nodes) != len(node_selected):
2404 # Report nodes that are not valid
2405 valid_node_names = [n.name for n in valid_nodes]
2406 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
2407 self.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names), node_active.name))
2409 # Reference original
2410 orig = node_active
2411 #node_selected_names = [n.name for n in node_selected]
2413 # Output list
2414 success_names = []
2416 # Deselect all nodes
2417 for i in node_selected:
2418 i.select = False
2420 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2421 # Run through all other nodes
2422 for node in valid_nodes[1:]:
2424 # Check for frame node
2425 parent = node.parent if node.parent else None
2426 node_loc = [node.location.x, node.location.y]
2428 # Select original to duplicate
2429 orig.select = True
2431 # Duplicate selected node
2432 bpy.ops.node.duplicate()
2433 new_node = context.selected_nodes[0]
2435 # Deselect copy
2436 new_node.select = False
2438 # Properties to copy
2439 node_tree = node.id_data
2440 props_to_copy = 'bl_idname name location height width'.split(' ')
2442 # Input and outputs
2443 reconnections = []
2444 mappings = chain.from_iterable([node.inputs, node.outputs])
2445 for i in (i for i in mappings if i.is_linked):
2446 for L in i.links:
2447 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
2449 # Properties
2450 props = {j: getattr(node, j) for j in props_to_copy}
2451 props_to_copy.pop(0)
2453 for prop in props_to_copy:
2454 setattr(new_node, prop, props[prop])
2456 # Get the node tree to remove the old node
2457 nodes = node_tree.nodes
2458 nodes.remove(node)
2459 new_node.name = props['name']
2461 if parent:
2462 new_node.parent = parent
2463 new_node.location = node_loc
2465 for str_from, str_to in reconnections:
2466 node_tree.links.new(eval(str_from), eval(str_to))
2468 success_names.append(new_node.name)
2470 orig.select = True
2471 node_tree.nodes.active = orig
2472 self.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig.name, ", ".join(success_names)))
2473 return {'FINISHED'}
2476 class NWCopyLabel(Operator, NWBase):
2477 bl_idname = "node.nw_copy_label"
2478 bl_label = "Copy Label"
2479 bl_options = {'REGISTER', 'UNDO'}
2481 option: EnumProperty(
2482 name="option",
2483 description="Source of name of label",
2484 items=(
2485 ('FROM_ACTIVE', 'from active', 'from active node',),
2486 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2487 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2491 def execute(self, context):
2492 nodes, links = get_nodes_links(context)
2493 option = self.option
2494 active = nodes.active
2495 if option == 'FROM_ACTIVE':
2496 if active:
2497 src_label = active.label
2498 for node in [n for n in nodes if n.select and nodes.active != n]:
2499 node.label = src_label
2500 elif option == 'FROM_NODE':
2501 selected = [n for n in nodes if n.select]
2502 for node in selected:
2503 for input in node.inputs:
2504 if input.links:
2505 src = input.links[0].from_node
2506 node.label = src.label
2507 break
2508 elif option == 'FROM_SOCKET':
2509 selected = [n for n in nodes if n.select]
2510 for node in selected:
2511 for input in node.inputs:
2512 if input.links:
2513 src = input.links[0].from_socket
2514 node.label = src.name
2515 break
2517 return {'FINISHED'}
2520 class NWClearLabel(Operator, NWBase):
2521 bl_idname = "node.nw_clear_label"
2522 bl_label = "Clear Label"
2523 bl_options = {'REGISTER', 'UNDO'}
2525 option: BoolProperty()
2527 def execute(self, context):
2528 nodes, links = get_nodes_links(context)
2529 for node in [n for n in nodes if n.select]:
2530 node.label = ''
2532 return {'FINISHED'}
2534 def invoke(self, context, event):
2535 if self.option:
2536 return self.execute(context)
2537 else:
2538 return context.window_manager.invoke_confirm(self, event)
2541 class NWModifyLabels(Operator, NWBase):
2542 """Modify Labels of all selected nodes"""
2543 bl_idname = "node.nw_modify_labels"
2544 bl_label = "Modify Labels"
2545 bl_options = {'REGISTER', 'UNDO'}
2547 prepend: StringProperty(
2548 name="Add to Beginning"
2550 append: StringProperty(
2551 name="Add to End"
2553 replace_from: StringProperty(
2554 name="Text to Replace"
2556 replace_to: StringProperty(
2557 name="Replace with"
2560 def execute(self, context):
2561 nodes, links = get_nodes_links(context)
2562 for node in [n for n in nodes if n.select]:
2563 node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
2565 return {'FINISHED'}
2567 def invoke(self, context, event):
2568 self.prepend = ""
2569 self.append = ""
2570 self.remove = ""
2571 return context.window_manager.invoke_props_dialog(self)
2574 class NWAddTextureSetup(Operator, NWBase):
2575 bl_idname = "node.nw_add_texture"
2576 bl_label = "Texture Setup"
2577 bl_description = "Add Texture Node Setup to Selected Shaders"
2578 bl_options = {'REGISTER', 'UNDO'}
2580 add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
2582 @classmethod
2583 def poll(cls, context):
2584 if nw_check(context):
2585 space = context.space_data
2586 if space.tree_type == 'ShaderNodeTree':
2587 return True
2588 return False
2590 def execute(self, context):
2591 nodes, links = get_nodes_links(context)
2593 texture_types = [x.nodetype for x in
2594 get_nodes_from_category('Texture', context)]
2595 selected_nodes = [n for n in nodes if n.select]
2597 for node in selected_nodes:
2598 if not node.inputs:
2599 continue
2601 input_index = 0
2602 target_input = node.inputs[0]
2603 for input in node.inputs:
2604 if input.enabled:
2605 input_index += 1
2606 if not input.is_linked:
2607 target_input = input
2608 break
2609 else:
2610 self.report({'WARNING'}, "No free inputs for node: " + node.name)
2611 continue
2613 x_offset = 0
2614 padding = 40.0
2615 locx = node.location.x
2616 locy = node.location.y - (input_index * padding)
2618 is_texture_node = node.rna_type.identifier in texture_types
2619 use_environment_texture = node.type == 'BACKGROUND'
2621 # Add an image texture before normal shader nodes.
2622 if not is_texture_node:
2623 image_texture_type = 'ShaderNodeTexEnvironment' if use_environment_texture else 'ShaderNodeTexImage'
2624 image_texture_node = nodes.new(image_texture_type)
2625 x_offset = x_offset + image_texture_node.width + padding
2626 image_texture_node.location = [locx - x_offset, locy]
2627 nodes.active = image_texture_node
2628 links.new(image_texture_node.outputs[0], target_input)
2630 # The mapping setup following this will connect to the first input of this image texture.
2631 target_input = image_texture_node.inputs[0]
2633 node.select = False
2635 if is_texture_node or self.add_mapping:
2636 # Add Mapping node.
2637 mapping_node = nodes.new('ShaderNodeMapping')
2638 x_offset = x_offset + mapping_node.width + padding
2639 mapping_node.location = [locx - x_offset, locy]
2640 links.new(mapping_node.outputs[0], target_input)
2642 # Add Texture Coordinates node.
2643 tex_coord_node = nodes.new('ShaderNodeTexCoord')
2644 x_offset = x_offset + tex_coord_node.width + padding
2645 tex_coord_node.location = [locx - x_offset, locy]
2647 is_procedural_texture = is_texture_node and node.type != 'TEX_IMAGE'
2648 use_generated_coordinates = is_procedural_texture or use_environment_texture
2649 tex_coord_output = tex_coord_node.outputs[0 if use_generated_coordinates else 2]
2650 links.new(tex_coord_output, mapping_node.inputs[0])
2652 return {'FINISHED'}
2655 class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
2656 bl_idname = "node.nw_add_textures_for_principled"
2657 bl_label = "Principled Texture Setup"
2658 bl_description = "Add Texture Node Setup for Principled BSDF"
2659 bl_options = {'REGISTER', 'UNDO'}
2661 directory: StringProperty(
2662 name='Directory',
2663 subtype='DIR_PATH',
2664 default='',
2665 description='Folder to search in for image files'
2667 files: CollectionProperty(
2668 type=bpy.types.OperatorFileListElement,
2669 options={'HIDDEN', 'SKIP_SAVE'}
2672 relative_path: BoolProperty(
2673 name='Relative Path',
2674 description='Set the file path relative to the blend file, when possible',
2675 default=True
2678 order = [
2679 "filepath",
2680 "files",
2683 def draw(self, context):
2684 layout = self.layout
2685 layout.alignment = 'LEFT'
2687 layout.prop(self, 'relative_path')
2689 @classmethod
2690 def poll(cls, context):
2691 valid = False
2692 if nw_check(context):
2693 space = context.space_data
2694 if space.tree_type == 'ShaderNodeTree':
2695 valid = True
2696 return valid
2698 def execute(self, context):
2699 # Check if everything is ok
2700 if not self.directory:
2701 self.report({'INFO'}, 'No Folder Selected')
2702 return {'CANCELLED'}
2703 if not self.files[:]:
2704 self.report({'INFO'}, 'No Files Selected')
2705 return {'CANCELLED'}
2707 nodes, links = get_nodes_links(context)
2708 active_node = nodes.active
2709 if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
2710 self.report({'INFO'}, 'Select Principled BSDF')
2711 return {'CANCELLED'}
2713 # Helper_functions
2714 def split_into__components(fname):
2715 # Split filename into components
2716 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2717 # Remove extension
2718 fname = path.splitext(fname)[0]
2719 # Remove digits
2720 fname = ''.join(i for i in fname if not i.isdigit())
2721 # Separate CamelCase by space
2722 fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>",fname)
2723 # Replace common separators with SPACE
2724 separators = ['_', '.', '-', '__', '--', '#']
2725 for sep in separators:
2726 fname = fname.replace(sep, ' ')
2728 components = fname.split(' ')
2729 components = [c.lower() for c in components]
2730 return components
2732 # Filter textures names for texturetypes in filenames
2733 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2734 tags = context.preferences.addons[__name__].preferences.principled_tags
2735 normal_abbr = tags.normal.split(' ')
2736 bump_abbr = tags.bump.split(' ')
2737 gloss_abbr = tags.gloss.split(' ')
2738 rough_abbr = tags.rough.split(' ')
2739 socketnames = [
2740 ['Displacement', tags.displacement.split(' '), None],
2741 ['Base Color', tags.base_color.split(' '), None],
2742 ['Subsurface Color', tags.sss_color.split(' '), None],
2743 ['Metallic', tags.metallic.split(' '), None],
2744 ['Specular', tags.specular.split(' '), None],
2745 ['Roughness', rough_abbr + gloss_abbr, None],
2746 ['Normal', normal_abbr + bump_abbr, None],
2747 ['Transmission', tags.transmission.split(' '), None],
2748 ['Emission', tags.emission.split(' '), None],
2749 ['Alpha', tags.alpha.split(' '), None],
2750 ['Ambient Occlusion', tags.ambient_occlusion.split(' '), None],
2753 # Look through texture_types and set value as filename of first matched file
2754 def match_files_to_socket_names():
2755 for sname in socketnames:
2756 for file in self.files:
2757 fname = file.name
2758 filenamecomponents = split_into__components(fname)
2759 matches = set(sname[1]).intersection(set(filenamecomponents))
2760 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2761 if matches:
2762 sname[2] = fname
2763 break
2765 match_files_to_socket_names()
2766 # Remove socketnames without found files
2767 socketnames = [s for s in socketnames if s[2]
2768 and path.exists(self.directory+s[2])]
2769 if not socketnames:
2770 self.report({'INFO'}, 'No matching images found')
2771 print('No matching images found')
2772 return {'CANCELLED'}
2774 # Don't override path earlier as os.path is used to check the absolute path
2775 import_path = self.directory
2776 if self.relative_path:
2777 if bpy.data.filepath:
2778 try:
2779 import_path = bpy.path.relpath(self.directory)
2780 except ValueError:
2781 pass
2783 # Add found images
2784 print('\nMatched Textures:')
2785 texture_nodes = []
2786 disp_texture = None
2787 ao_texture = None
2788 normal_node = None
2789 roughness_node = None
2790 for i, sname in enumerate(socketnames):
2791 print(i, sname[0], sname[2])
2793 # DISPLACEMENT NODES
2794 if sname[0] == 'Displacement':
2795 disp_texture = nodes.new(type='ShaderNodeTexImage')
2796 img = bpy.data.images.load(path.join(import_path, sname[2]))
2797 disp_texture.image = img
2798 disp_texture.label = 'Displacement'
2799 if disp_texture.image:
2800 disp_texture.image.colorspace_settings.is_data = True
2802 # Add displacement offset nodes
2803 disp_node = nodes.new(type='ShaderNodeDisplacement')
2804 # Align the Displacement node under the active Principled BSDF node
2805 disp_node.location = active_node.location + Vector((100, -700))
2806 link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
2808 # TODO Turn on true displacement in the material
2809 # Too complicated for now
2811 # Find output node
2812 output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
2813 if output_node:
2814 if not output_node[0].inputs[2].is_linked:
2815 link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
2817 continue
2819 # AMBIENT OCCLUSION TEXTURE
2820 if sname[0] == 'Ambient Occlusion':
2821 ao_texture = nodes.new(type='ShaderNodeTexImage')
2822 img = bpy.data.images.load(path.join(import_path, sname[2]))
2823 ao_texture.image = img
2824 ao_texture.label = sname[0]
2825 if ao_texture.image:
2826 ao_texture.image.colorspace_settings.is_data = True
2828 continue
2830 if not active_node.inputs[sname[0]].is_linked:
2831 # No texture node connected -> add texture node with new image
2832 texture_node = nodes.new(type='ShaderNodeTexImage')
2833 img = bpy.data.images.load(path.join(import_path, sname[2]))
2834 texture_node.image = img
2836 # NORMAL NODES
2837 if sname[0] == 'Normal':
2838 # Test if new texture node is normal or bump map
2839 fname_components = split_into__components(sname[2])
2840 match_normal = set(normal_abbr).intersection(set(fname_components))
2841 match_bump = set(bump_abbr).intersection(set(fname_components))
2842 if match_normal:
2843 # If Normal add normal node in between
2844 normal_node = nodes.new(type='ShaderNodeNormalMap')
2845 link = links.new(normal_node.inputs[1], texture_node.outputs[0])
2846 elif match_bump:
2847 # If Bump add bump node in between
2848 normal_node = nodes.new(type='ShaderNodeBump')
2849 link = links.new(normal_node.inputs[2], texture_node.outputs[0])
2851 link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
2852 normal_node_texture = texture_node
2854 elif sname[0] == 'Roughness':
2855 # Test if glossy or roughness map
2856 fname_components = split_into__components(sname[2])
2857 match_rough = set(rough_abbr).intersection(set(fname_components))
2858 match_gloss = set(gloss_abbr).intersection(set(fname_components))
2860 if match_rough:
2861 # If Roughness nothing to to
2862 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2864 elif match_gloss:
2865 # If Gloss Map add invert node
2866 invert_node = nodes.new(type='ShaderNodeInvert')
2867 link = links.new(invert_node.inputs[1], texture_node.outputs[0])
2869 link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
2870 roughness_node = texture_node
2872 else:
2873 # This is a simple connection Texture --> Input slot
2874 link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
2876 # Use non-color for all but 'Base Color' Textures
2877 if not sname[0] in ['Base Color', 'Emission'] and texture_node.image:
2878 texture_node.image.colorspace_settings.is_data = True
2880 else:
2881 # If already texture connected. add to node list for alignment
2882 texture_node = active_node.inputs[sname[0]].links[0].from_node
2884 # This are all connected texture nodes
2885 texture_nodes.append(texture_node)
2886 texture_node.label = sname[0]
2888 if disp_texture:
2889 texture_nodes.append(disp_texture)
2891 if ao_texture:
2892 # We want the ambient occlusion texture to be the top most texture node
2893 texture_nodes.insert(0, ao_texture)
2895 # Alignment
2896 for i, texture_node in enumerate(texture_nodes):
2897 offset = Vector((-550, (i * -280) + 200))
2898 texture_node.location = active_node.location + offset
2900 if normal_node:
2901 # Extra alignment if normal node was added
2902 normal_node.location = normal_node_texture.location + Vector((300, 0))
2904 if roughness_node:
2905 # Alignment of invert node if glossy map
2906 invert_node.location = roughness_node.location + Vector((300, 0))
2908 # Add texture input + mapping
2909 mapping = nodes.new(type='ShaderNodeMapping')
2910 mapping.location = active_node.location + Vector((-1050, 0))
2911 if len(texture_nodes) > 1:
2912 # If more than one texture add reroute node in between
2913 reroute = nodes.new(type='NodeReroute')
2914 texture_nodes.append(reroute)
2915 tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
2916 reroute.location = tex_coords + Vector((-50, -120))
2917 for texture_node in texture_nodes:
2918 link = links.new(texture_node.inputs[0], reroute.outputs[0])
2919 link = links.new(reroute.inputs[0], mapping.outputs[0])
2920 else:
2921 link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
2923 # Connect texture_coordiantes to mapping node
2924 texture_input = nodes.new(type='ShaderNodeTexCoord')
2925 texture_input.location = mapping.location + Vector((-200, 0))
2926 link = links.new(mapping.inputs[0], texture_input.outputs[2])
2928 # Create frame around tex coords and mapping
2929 frame = nodes.new(type='NodeFrame')
2930 frame.label = 'Mapping'
2931 mapping.parent = frame
2932 texture_input.parent = frame
2933 frame.update()
2935 # Create frame around texture nodes
2936 frame = nodes.new(type='NodeFrame')
2937 frame.label = 'Textures'
2938 for tnode in texture_nodes:
2939 tnode.parent = frame
2940 frame.update()
2942 # Just to be sure
2943 active_node.select = False
2944 nodes.update()
2945 links.update()
2946 force_update(context)
2947 return {'FINISHED'}
2950 class NWAddReroutes(Operator, NWBase):
2951 """Add Reroute Nodes and link them to outputs of selected nodes"""
2952 bl_idname = "node.nw_add_reroutes"
2953 bl_label = "Add Reroutes"
2954 bl_description = "Add Reroutes to Outputs"
2955 bl_options = {'REGISTER', 'UNDO'}
2957 option: EnumProperty(
2958 name="option",
2959 items=[
2960 ('ALL', 'to all', 'Add to all outputs'),
2961 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2962 ('LINKED', 'to linked', 'Add only to linked outputs'),
2966 def execute(self, context):
2967 tree_type = context.space_data.node_tree.type
2968 option = self.option
2969 nodes, links = get_nodes_links(context)
2970 # output valid when option is 'all' or when 'loose' output has no links
2971 valid = False
2972 post_select = [] # nodes to be selected after execution
2973 # create reroutes and recreate links
2974 for node in [n for n in nodes if n.select]:
2975 if node.outputs:
2976 x = node.location.x
2977 y = node.location.y
2978 width = node.width
2979 # unhide 'REROUTE' nodes to avoid issues with location.y
2980 if node.type == 'REROUTE':
2981 node.hide = False
2982 # When node is hidden - width_hidden not usable.
2983 # Hack needed to calculate real width
2984 if node.hide:
2985 bpy.ops.node.select_all(action='DESELECT')
2986 helper = nodes.new('NodeReroute')
2987 helper.select = True
2988 node.select = True
2989 # resize node and helper to zero. Then check locations to calculate width
2990 bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
2991 width = 2.0 * (helper.location.x - node.location.x)
2992 # restore node location
2993 node.location = x, y
2994 # delete helper
2995 node.select = False
2996 # only helper is selected now
2997 bpy.ops.node.delete()
2998 x = node.location.x + width + 20.0
2999 if node.type != 'REROUTE':
3000 y -= 35.0
3001 y_offset = -22.0
3002 loc = x, y
3003 reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
3004 for out_i, output in enumerate(node.outputs):
3005 pass_used = False # initial value to be analyzed if 'R_LAYERS'
3006 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3007 if node.type != 'R_LAYERS':
3008 pass_used = True
3009 else: # if 'R_LAYERS' check if output represent used render pass
3010 node_scene = node.scene
3011 node_layer = node.layer
3012 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3013 if output.name == 'Alpha':
3014 pass_used = True
3015 else:
3016 # check entries in global 'rl_outputs' variable
3017 for rlo in rl_outputs:
3018 if output.name in {rlo.output_name, rlo.exr_output_name}:
3019 pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
3020 break
3021 if pass_used:
3022 valid = ((option == 'ALL') or
3023 (option == 'LOOSE' and not output.links) or
3024 (option == 'LINKED' and output.links))
3025 # Add reroutes only if valid, but offset location in all cases.
3026 if valid:
3027 n = nodes.new('NodeReroute')
3028 nodes.active = n
3029 for link in output.links:
3030 links.new(n.outputs[0], link.to_socket)
3031 links.new(output, n.inputs[0])
3032 n.location = loc
3033 post_select.append(n)
3034 reroutes_count += 1
3035 y += y_offset
3036 loc = x, y
3037 # disselect the node so that after execution of script only newly created nodes are selected
3038 node.select = False
3039 # nicer reroutes distribution along y when node.hide
3040 if node.hide:
3041 y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
3042 for reroute in [r for r in nodes if r.select]:
3043 reroute.location.y -= y_translate
3044 for node in post_select:
3045 node.select = True
3047 return {'FINISHED'}
3050 class NWLinkActiveToSelected(Operator, NWBase):
3051 """Link active node to selected nodes basing on various criteria"""
3052 bl_idname = "node.nw_link_active_to_selected"
3053 bl_label = "Link Active Node to Selected"
3054 bl_options = {'REGISTER', 'UNDO'}
3056 replace: BoolProperty()
3057 use_node_name: BoolProperty()
3058 use_outputs_names: BoolProperty()
3060 @classmethod
3061 def poll(cls, context):
3062 valid = False
3063 if nw_check(context):
3064 if context.active_node is not None:
3065 if context.active_node.select:
3066 valid = True
3067 return valid
3069 def execute(self, context):
3070 nodes, links = get_nodes_links(context)
3071 replace = self.replace
3072 use_node_name = self.use_node_name
3073 use_outputs_names = self.use_outputs_names
3074 active = nodes.active
3075 selected = [node for node in nodes if node.select and node != active]
3076 outputs = [] # Only usable outputs of active nodes will be stored here.
3077 for out in active.outputs:
3078 if active.type != 'R_LAYERS':
3079 outputs.append(out)
3080 else:
3081 # 'R_LAYERS' node type needs special handling.
3082 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3083 # Only outputs that represent used passes should be taken into account
3084 # Check if pass represented by output is used.
3085 # global 'rl_outputs' list will be used for that
3086 for rlo in rl_outputs:
3087 pass_used = False # initial value. Will be set to True if pass is used
3088 if out.name == 'Alpha':
3089 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3090 pass_used = True
3091 elif out.name in {rlo.output_name, rlo.exr_output_name}:
3092 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3093 pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
3094 break
3095 if pass_used:
3096 outputs.append(out)
3097 doit = True # Will be changed to False when links successfully added to previous output.
3098 for out in outputs:
3099 if doit:
3100 for node in selected:
3101 dst_name = node.name # Will be compared with src_name if needed.
3102 # When node has label - use it as dst_name
3103 if node.label:
3104 dst_name = node.label
3105 valid = True # Initial value. Will be changed to False if names don't match.
3106 src_name = dst_name # If names not used - this assignment will keep valid = True.
3107 if use_node_name:
3108 # Set src_name to source node name or label
3109 src_name = active.name
3110 if active.label:
3111 src_name = active.label
3112 elif use_outputs_names:
3113 src_name = (out.name, )
3114 for rlo in rl_outputs:
3115 if out.name in {rlo.output_name, rlo.exr_output_name}:
3116 src_name = (rlo.output_name, rlo.exr_output_name)
3117 if dst_name not in src_name:
3118 valid = False
3119 if valid:
3120 for input in node.inputs:
3121 if input.type == out.type or node.type == 'REROUTE':
3122 if replace or not input.is_linked:
3123 links.new(out, input)
3124 if not use_node_name and not use_outputs_names:
3125 doit = False
3126 break
3128 return {'FINISHED'}
3131 class NWAlignNodes(Operator, NWBase):
3132 '''Align the selected nodes neatly in a row/column'''
3133 bl_idname = "node.nw_align_nodes"
3134 bl_label = "Align Nodes"
3135 bl_options = {'REGISTER', 'UNDO'}
3136 margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
3138 def execute(self, context):
3139 nodes, links = get_nodes_links(context)
3140 margin = self.margin
3142 selection = []
3143 for node in nodes:
3144 if node.select and node.type != 'FRAME':
3145 selection.append(node)
3147 # If no nodes are selected, align all nodes
3148 active_loc = None
3149 if not selection:
3150 selection = nodes
3151 elif nodes.active in selection:
3152 active_loc = copy(nodes.active.location) # make a copy, not a reference
3154 # Check if nodes should be laid out horizontally or vertically
3155 x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
3156 y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
3157 x_range = max(x_locs) - min(x_locs)
3158 y_range = max(y_locs) - min(y_locs)
3159 mid_x = (max(x_locs) + min(x_locs)) / 2
3160 mid_y = (max(y_locs) + min(y_locs)) / 2
3161 horizontal = x_range > y_range
3163 # Sort selection by location of node mid-point
3164 if horizontal:
3165 selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
3166 else:
3167 selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
3169 # Alignment
3170 current_pos = 0
3171 for node in selection:
3172 current_margin = margin
3173 current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
3175 if horizontal:
3176 node.location.x = current_pos
3177 current_pos += current_margin + node.dimensions.x
3178 node.location.y = mid_y + (node.dimensions.y / 2)
3179 else:
3180 node.location.y = current_pos
3181 current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
3182 node.location.x = mid_x - (node.dimensions.x / 2)
3184 # If active node is selected, center nodes around it
3185 if active_loc is not None:
3186 active_loc_diff = active_loc - nodes.active.location
3187 for node in selection:
3188 node.location += active_loc_diff
3189 else: # Position nodes centered around where they used to be
3190 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])
3191 new_mid = (max(locs) + min(locs)) / 2
3192 for node in selection:
3193 if horizontal:
3194 node.location.x += (mid_x - new_mid)
3195 else:
3196 node.location.y += (mid_y - new_mid)
3198 return {'FINISHED'}
3201 class NWSelectParentChildren(Operator, NWBase):
3202 bl_idname = "node.nw_select_parent_child"
3203 bl_label = "Select Parent or Children"
3204 bl_options = {'REGISTER', 'UNDO'}
3206 option: EnumProperty(
3207 name="option",
3208 items=(
3209 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3210 ('CHILD', 'Select Children', 'Select members of selected frame'),
3214 def execute(self, context):
3215 nodes, links = get_nodes_links(context)
3216 option = self.option
3217 selected = [node for node in nodes if node.select]
3218 if option == 'PARENT':
3219 for sel in selected:
3220 parent = sel.parent
3221 if parent:
3222 parent.select = True
3223 else: # option == 'CHILD'
3224 for sel in selected:
3225 children = [node for node in nodes if node.parent == sel]
3226 for kid in children:
3227 kid.select = True
3229 return {'FINISHED'}
3232 class NWDetachOutputs(Operator, NWBase):
3233 """Detach outputs of selected node leaving inputs linked"""
3234 bl_idname = "node.nw_detach_outputs"
3235 bl_label = "Detach Outputs"
3236 bl_options = {'REGISTER', 'UNDO'}
3238 def execute(self, context):
3239 nodes, links = get_nodes_links(context)
3240 selected = context.selected_nodes
3241 bpy.ops.node.duplicate_move_keep_inputs()
3242 new_nodes = context.selected_nodes
3243 bpy.ops.node.select_all(action="DESELECT")
3244 for node in selected:
3245 node.select = True
3246 bpy.ops.node.delete_reconnect()
3247 for new_node in new_nodes:
3248 new_node.select = True
3249 bpy.ops.transform.translate('INVOKE_DEFAULT')
3251 return {'FINISHED'}
3254 class NWLinkToOutputNode(Operator):
3255 """Link to Composite node or Material Output node"""
3256 bl_idname = "node.nw_link_out"
3257 bl_label = "Connect to Output"
3258 bl_options = {'REGISTER', 'UNDO'}
3260 @classmethod
3261 def poll(cls, context):
3262 valid = False
3263 if nw_check(context):
3264 if context.active_node is not None:
3265 for out in context.active_node.outputs:
3266 if is_visible_socket(out):
3267 valid = True
3268 break
3269 return valid
3271 def execute(self, context):
3272 nodes, links = get_nodes_links(context)
3273 active = nodes.active
3274 output_index = None
3275 tree_type = context.space_data.tree_type
3276 shader_outputs = {'OBJECT': 'ShaderNodeOutputMaterial',
3277 'WORLD': 'ShaderNodeOutputWorld',
3278 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
3279 output_type = {
3280 'ShaderNodeTree': shader_outputs[context.space_data.shader_type],
3281 'CompositorNodeTree': 'CompositorNodeComposite',
3282 'TextureNodeTree': 'TextureNodeOutput',
3283 'GeometryNodeTree': 'NodeGroupOutput',
3284 }[tree_type]
3285 for node in nodes:
3286 # check whether the node is an output node and,
3287 # if supported, whether it's the active one
3288 if node.rna_type.identifier == output_type \
3289 and (node.is_active_output if hasattr(node, 'is_active_output')
3290 else True):
3291 output_node = node
3292 break
3293 else: # No output node exists
3294 bpy.ops.node.select_all(action="DESELECT")
3295 output_node = nodes.new(output_type)
3296 output_node.location.x = active.location.x + active.dimensions.x + 80
3297 output_node.location.y = active.location.y
3299 if active.outputs:
3300 for i, output in enumerate(active.outputs):
3301 if is_visible_socket(output):
3302 output_index = i
3303 break
3304 for i, output in enumerate(active.outputs):
3305 if output.type == output_node.inputs[0].type and is_visible_socket(output):
3306 output_index = i
3307 break
3309 out_input_index = 0
3310 if tree_type == 'ShaderNodeTree':
3311 if active.outputs[output_index].name == 'Volume':
3312 out_input_index = 1
3313 elif active.outputs[output_index].name == 'Displacement':
3314 out_input_index = 2
3315 elif tree_type == 'GeometryNodeTree':
3316 if active.outputs[output_index].type != 'GEOMETRY':
3317 return {'CANCELLED'}
3318 links.new(active.outputs[output_index], output_node.inputs[out_input_index])
3320 force_update(context) # viewport render does not update
3322 return {'FINISHED'}
3325 class NWMakeLink(Operator, NWBase):
3326 """Make a link from one socket to another"""
3327 bl_idname = 'node.nw_make_link'
3328 bl_label = 'Make Link'
3329 bl_options = {'REGISTER', 'UNDO'}
3330 from_socket: IntProperty()
3331 to_socket: IntProperty()
3333 def execute(self, context):
3334 nodes, links = get_nodes_links(context)
3336 n1 = nodes[context.scene.NWLazySource]
3337 n2 = nodes[context.scene.NWLazyTarget]
3339 links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
3341 force_update(context)
3343 return {'FINISHED'}
3346 class NWCallInputsMenu(Operator, NWBase):
3347 """Link from this output"""
3348 bl_idname = 'node.nw_call_inputs_menu'
3349 bl_label = 'Make Link'
3350 bl_options = {'REGISTER', 'UNDO'}
3351 from_socket: IntProperty()
3353 def execute(self, context):
3354 nodes, links = get_nodes_links(context)
3356 context.scene.NWSourceSocket = self.from_socket
3358 n1 = nodes[context.scene.NWLazySource]
3359 n2 = nodes[context.scene.NWLazyTarget]
3360 if len(n2.inputs) > 1:
3361 bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
3362 elif len(n2.inputs) == 1:
3363 links.new(n1.outputs[self.from_socket], n2.inputs[0])
3364 return {'FINISHED'}
3367 class NWAddSequence(Operator, NWBase, ImportHelper):
3368 """Add an Image Sequence"""
3369 bl_idname = 'node.nw_add_sequence'
3370 bl_label = 'Import Image Sequence'
3371 bl_options = {'REGISTER', 'UNDO'}
3373 directory: StringProperty(
3374 subtype="DIR_PATH"
3376 filename: StringProperty(
3377 subtype="FILE_NAME"
3379 files: CollectionProperty(
3380 type=bpy.types.OperatorFileListElement,
3381 options={'HIDDEN', 'SKIP_SAVE'}
3383 relative_path: BoolProperty(
3384 name='Relative Path',
3385 description='Set the file path relative to the blend file, when possible',
3386 default=True
3389 def draw(self, context):
3390 layout = self.layout
3391 layout.alignment = 'LEFT'
3393 layout.prop(self, 'relative_path')
3395 def execute(self, context):
3396 nodes, links = get_nodes_links(context)
3397 directory = self.directory
3398 filename = self.filename
3399 files = self.files
3400 tree = context.space_data.node_tree
3402 # DEBUG
3403 # print ("\nDIR:", directory)
3404 # print ("FN:", filename)
3405 # print ("Fs:", list(f.name for f in files), '\n')
3407 if tree.type == 'SHADER':
3408 node_type = "ShaderNodeTexImage"
3409 elif tree.type == 'COMPOSITING':
3410 node_type = "CompositorNodeImage"
3411 else:
3412 self.report({'ERROR'}, "Unsupported Node Tree type!")
3413 return {'CANCELLED'}
3415 if not files[0].name and not filename:
3416 self.report({'ERROR'}, "No file chosen")
3417 return {'CANCELLED'}
3418 elif files[0].name and (not filename or not path.exists(directory+filename)):
3419 # User has selected multiple files without an active one, or the active one is non-existent
3420 filename = files[0].name
3422 if not path.exists(directory+filename):
3423 self.report({'ERROR'}, filename+" does not exist!")
3424 return {'CANCELLED'}
3426 without_ext = '.'.join(filename.split('.')[:-1])
3428 # if last digit isn't a number, it's not a sequence
3429 if not without_ext[-1].isdigit():
3430 self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
3431 return {'CANCELLED'}
3434 extension = filename.split('.')[-1]
3435 reverse = without_ext[::-1] # reverse string
3437 count_numbers = 0
3438 for char in reverse:
3439 if char.isdigit():
3440 count_numbers += 1
3441 else:
3442 break
3444 without_num = without_ext[:count_numbers*-1]
3446 files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
3448 num_frames = len(files)
3450 nodes_list = [node for node in nodes]
3451 if nodes_list:
3452 nodes_list.sort(key=lambda k: k.location.x)
3453 xloc = nodes_list[0].location.x - 220 # place new nodes at far left
3454 yloc = 0
3455 for node in nodes:
3456 node.select = False
3457 yloc += node_mid_pt(node, 'y')
3458 yloc = yloc/len(nodes)
3459 else:
3460 xloc = 0
3461 yloc = 0
3463 name_with_hashes = without_num + "#"*count_numbers + '.' + extension
3465 bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
3466 node = nodes.active
3467 node.label = name_with_hashes
3469 filepath = directory+(without_ext+'.'+extension)
3470 if self.relative_path:
3471 if bpy.data.filepath:
3472 try:
3473 filepath = bpy.path.relpath(filepath)
3474 except ValueError:
3475 pass
3477 img = bpy.data.images.load(filepath)
3478 img.source = 'SEQUENCE'
3479 img.name = name_with_hashes
3480 node.image = img
3481 image_user = node.image_user if tree.type == 'SHADER' else node
3482 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
3483 image_user.frame_duration = num_frames
3485 return {'FINISHED'}
3488 class NWAddMultipleImages(Operator, NWBase, ImportHelper):
3489 """Add multiple images at once"""
3490 bl_idname = 'node.nw_add_multiple_images'
3491 bl_label = 'Open Selected Images'
3492 bl_options = {'REGISTER', 'UNDO'}
3493 directory: StringProperty(
3494 subtype="DIR_PATH"
3496 files: CollectionProperty(
3497 type=bpy.types.OperatorFileListElement,
3498 options={'HIDDEN', 'SKIP_SAVE'}
3501 def execute(self, context):
3502 nodes, links = get_nodes_links(context)
3504 xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3506 if context.space_data.node_tree.type == 'SHADER':
3507 node_type = "ShaderNodeTexImage"
3508 elif context.space_data.node_tree.type == 'COMPOSITING':
3509 node_type = "CompositorNodeImage"
3510 else:
3511 self.report({'ERROR'}, "Unsupported Node Tree type!")
3512 return {'CANCELLED'}
3514 new_nodes = []
3515 for f in self.files:
3516 fname = f.name
3518 node = nodes.new(node_type)
3519 new_nodes.append(node)
3520 node.label = fname
3521 node.hide = True
3522 node.width_hidden = 100
3523 node.location.x = xloc
3524 node.location.y = yloc
3525 yloc -= 40
3527 img = bpy.data.images.load(self.directory+fname)
3528 node.image = img
3530 # shift new nodes up to center of tree
3531 list_size = new_nodes[0].location.y - new_nodes[-1].location.y
3532 for node in nodes:
3533 if node in new_nodes:
3534 node.select = True
3535 node.location.y += (list_size/2)
3536 else:
3537 node.select = False
3538 return {'FINISHED'}
3541 class NWViewerFocus(bpy.types.Operator):
3542 """Set the viewer tile center to the mouse position"""
3543 bl_idname = "node.nw_viewer_focus"
3544 bl_label = "Viewer Focus"
3546 x: bpy.props.IntProperty()
3547 y: bpy.props.IntProperty()
3549 @classmethod
3550 def poll(cls, context):
3551 return nw_check(context) and context.space_data.tree_type == 'CompositorNodeTree'
3553 def execute(self, context):
3554 return {'FINISHED'}
3556 def invoke(self, context, event):
3557 render = context.scene.render
3558 space = context.space_data
3559 percent = render.resolution_percentage*0.01
3561 nodes, links = get_nodes_links(context)
3562 viewers = [n for n in nodes if n.type == 'VIEWER']
3564 if viewers:
3565 mlocx = event.mouse_region_x
3566 mlocy = event.mouse_region_y
3567 select_node = bpy.ops.node.select(location=(mlocx, mlocy), extend=False)
3569 if not 'FINISHED' in select_node: # only run if we're not clicking on a node
3570 region_x = context.region.width
3571 region_y = context.region.height
3573 region_center_x = context.region.width / 2
3574 region_center_y = context.region.height / 2
3576 bd_x = render.resolution_x * percent * space.backdrop_zoom
3577 bd_y = render.resolution_y * percent * space.backdrop_zoom
3579 backdrop_center_x = (bd_x / 2) - space.backdrop_offset[0]
3580 backdrop_center_y = (bd_y / 2) - space.backdrop_offset[1]
3582 margin_x = region_center_x - backdrop_center_x
3583 margin_y = region_center_y - backdrop_center_y
3585 abs_mouse_x = (mlocx - margin_x) / bd_x
3586 abs_mouse_y = (mlocy - margin_y) / bd_y
3588 for node in viewers:
3589 node.center_x = abs_mouse_x
3590 node.center_y = abs_mouse_y
3591 else:
3592 return {'PASS_THROUGH'}
3594 return self.execute(context)
3597 class NWSaveViewer(bpy.types.Operator, ExportHelper):
3598 """Save the current viewer node to an image file"""
3599 bl_idname = "node.nw_save_viewer"
3600 bl_label = "Save This Image"
3601 filepath: StringProperty(subtype="FILE_PATH")
3602 filename_ext: EnumProperty(
3603 name="Format",
3604 description="Choose the file format to save to",
3605 items=(('.bmp', "BMP", ""),
3606 ('.rgb', 'IRIS', ""),
3607 ('.png', 'PNG', ""),
3608 ('.jpg', 'JPEG', ""),
3609 ('.jp2', 'JPEG2000', ""),
3610 ('.tga', 'TARGA', ""),
3611 ('.cin', 'CINEON', ""),
3612 ('.dpx', 'DPX', ""),
3613 ('.exr', 'OPEN_EXR', ""),
3614 ('.hdr', 'HDR', ""),
3615 ('.tif', 'TIFF', "")),
3616 default='.png',
3619 @classmethod
3620 def poll(cls, context):
3621 valid = False
3622 if nw_check(context):
3623 if context.space_data.tree_type == 'CompositorNodeTree':
3624 if "Viewer Node" in [i.name for i in bpy.data.images]:
3625 if sum(bpy.data.images["Viewer Node"].size) > 0: # False if not connected or connected but no image
3626 valid = True
3627 return valid
3629 def execute(self, context):
3630 fp = self.filepath
3631 if fp:
3632 formats = {
3633 '.bmp': 'BMP',
3634 '.rgb': 'IRIS',
3635 '.png': 'PNG',
3636 '.jpg': 'JPEG',
3637 '.jpeg': 'JPEG',
3638 '.jp2': 'JPEG2000',
3639 '.tga': 'TARGA',
3640 '.cin': 'CINEON',
3641 '.dpx': 'DPX',
3642 '.exr': 'OPEN_EXR',
3643 '.hdr': 'HDR',
3644 '.tiff': 'TIFF',
3645 '.tif': 'TIFF'}
3646 basename, ext = path.splitext(fp)
3647 old_render_format = context.scene.render.image_settings.file_format
3648 context.scene.render.image_settings.file_format = formats[self.filename_ext]
3649 context.area.type = "IMAGE_EDITOR"
3650 context.area.spaces[0].image = bpy.data.images['Viewer Node']
3651 context.area.spaces[0].image.save_render(fp)
3652 context.area.type = "NODE_EDITOR"
3653 context.scene.render.image_settings.file_format = old_render_format
3654 return {'FINISHED'}
3657 class NWResetNodes(bpy.types.Operator):
3658 """Reset Nodes in Selection"""
3659 bl_idname = "node.nw_reset_nodes"
3660 bl_label = "Reset Nodes"
3661 bl_options = {'REGISTER', 'UNDO'}
3663 @classmethod
3664 def poll(cls, context):
3665 space = context.space_data
3666 return space.type == 'NODE_EDITOR'
3668 def execute(self, context):
3669 node_active = context.active_node
3670 node_selected = context.selected_nodes
3671 node_ignore = ["FRAME","REROUTE", "GROUP"]
3673 # Check if one node is selected at least
3674 if not (len(node_selected) > 0):
3675 self.report({'ERROR'}, "1 node must be selected at least")
3676 return {'CANCELLED'}
3678 active_node_name = node_active.name if node_active.select else None
3679 valid_nodes = [n for n in node_selected if n.type not in node_ignore]
3681 # Create output lists
3682 selected_node_names = [n.name for n in node_selected]
3683 success_names = []
3685 # Reset all valid children in a frame
3686 node_active_is_frame = False
3687 if len(node_selected) == 1 and node_active.type == "FRAME":
3688 node_tree = node_active.id_data
3689 children = [n for n in node_tree.nodes if n.parent == node_active]
3690 if children:
3691 valid_nodes = [n for n in children if n.type not in node_ignore]
3692 selected_node_names = [n.name for n in children if n.type not in node_ignore]
3693 node_active_is_frame = True
3695 # Check if valid nodes in selection
3696 if not (len(valid_nodes) > 0):
3697 # Check for frames only
3698 frames_selected = [n for n in node_selected if n.type == "FRAME"]
3699 if (len(frames_selected) > 1 and len(frames_selected) == len(node_selected)):
3700 self.report({'ERROR'}, "Please select only 1 frame to reset")
3701 else:
3702 self.report({'ERROR'}, "No valid node(s) in selection")
3703 return {'CANCELLED'}
3705 # Report nodes that are not valid
3706 if len(valid_nodes) != len(node_selected) and node_active_is_frame is False:
3707 valid_node_names = [n.name for n in valid_nodes]
3708 not_valid_names = list(set(selected_node_names) - set(valid_node_names))
3709 self.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names)))
3711 # Deselect all nodes
3712 for i in node_selected:
3713 i.select = False
3715 # Run through all valid nodes
3716 for node in valid_nodes:
3718 parent = node.parent if node.parent else None
3719 node_loc = [node.location.x, node.location.y]
3721 node_tree = node.id_data
3722 props_to_copy = 'bl_idname name location height width'.split(' ')
3724 reconnections = []
3725 mappings = chain.from_iterable([node.inputs, node.outputs])
3726 for i in (i for i in mappings if i.is_linked):
3727 for L in i.links:
3728 reconnections.append([L.from_socket.path_from_id(), L.to_socket.path_from_id()])
3730 props = {j: getattr(node, j) for j in props_to_copy}
3732 new_node = node_tree.nodes.new(props['bl_idname'])
3733 props_to_copy.pop(0)
3735 for prop in props_to_copy:
3736 setattr(new_node, prop, props[prop])
3738 nodes = node_tree.nodes
3739 nodes.remove(node)
3740 new_node.name = props['name']
3742 if parent:
3743 new_node.parent = parent
3744 new_node.location = node_loc
3746 for str_from, str_to in reconnections:
3747 node_tree.links.new(eval(str_from), eval(str_to))
3749 new_node.select = False
3750 success_names.append(new_node.name)
3752 # Reselect all nodes
3753 if selected_node_names and node_active_is_frame is False:
3754 for i in selected_node_names:
3755 node_tree.nodes[i].select = True
3757 if active_node_name is not None:
3758 node_tree.nodes[active_node_name].select = True
3759 node_tree.nodes.active = node_tree.nodes[active_node_name]
3761 self.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names)))
3762 return {'FINISHED'}
3766 # P A N E L
3769 def drawlayout(context, layout, mode='non-panel'):
3770 tree_type = context.space_data.tree_type
3772 col = layout.column(align=True)
3773 col.menu(NWMergeNodesMenu.bl_idname)
3774 col.separator()
3776 col = layout.column(align=True)
3777 col.menu(NWSwitchNodeTypeMenu.bl_idname, text="Switch Node Type")
3778 col.separator()
3780 if tree_type == 'ShaderNodeTree':
3781 col = layout.column(align=True)
3782 col.operator(NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
3783 col.operator(NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
3784 col.separator()
3786 col = layout.column(align=True)
3787 col.operator(NWDetachOutputs.bl_idname, icon='UNLINKED')
3788 col.operator(NWSwapLinks.bl_idname)
3789 col.menu(NWAddReroutesMenu.bl_idname, text="Add Reroutes", icon='LAYER_USED')
3790 col.separator()
3792 col = layout.column(align=True)
3793 col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", icon='LINKED')
3794 if tree_type != 'GeometryNodeTree':
3795 col.operator(NWLinkToOutputNode.bl_idname, icon='DRIVER')
3796 col.separator()
3798 col = layout.column(align=True)
3799 if mode == 'panel':
3800 row = col.row(align=True)
3801 row.operator(NWClearLabel.bl_idname).option = True
3802 row.operator(NWModifyLabels.bl_idname)
3803 else:
3804 col.operator(NWClearLabel.bl_idname).option = True
3805 col.operator(NWModifyLabels.bl_idname)
3806 col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change")
3807 col.separator()
3808 col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
3809 col.separator()
3811 col = layout.column(align=True)
3812 if tree_type == 'CompositorNodeTree':
3813 col.operator(NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
3814 if tree_type != 'GeometryNodeTree':
3815 col.operator(NWReloadImages.bl_idname, icon='FILE_REFRESH')
3816 col.separator()
3818 col = layout.column(align=True)
3819 col.operator(NWFrameSelected.bl_idname, icon='STICKY_UVS_LOC')
3820 col.separator()
3822 col = layout.column(align=True)
3823 col.operator(NWAlignNodes.bl_idname, icon='CENTER_ONLY')
3824 col.separator()
3826 col = layout.column(align=True)
3827 col.operator(NWDeleteUnused.bl_idname, icon='CANCEL')
3828 col.separator()
3831 class NodeWranglerPanel(Panel, NWBase):
3832 bl_idname = "NODE_PT_nw_node_wrangler"
3833 bl_space_type = 'NODE_EDITOR'
3834 bl_label = "Node Wrangler"
3835 bl_region_type = "UI"
3836 bl_category = "Node Wrangler"
3838 prepend: StringProperty(
3839 name='prepend',
3841 append: StringProperty()
3842 remove: StringProperty()
3844 def draw(self, context):
3845 self.layout.label(text="(Quick access: Shift+W)")
3846 drawlayout(context, self.layout, mode='panel')
3850 # M E N U S
3852 class NodeWranglerMenu(Menu, NWBase):
3853 bl_idname = "NODE_MT_nw_node_wrangler_menu"
3854 bl_label = "Node Wrangler"
3856 def draw(self, context):
3857 self.layout.operator_context = 'INVOKE_DEFAULT'
3858 drawlayout(context, self.layout)
3861 class NWMergeNodesMenu(Menu, NWBase):
3862 bl_idname = "NODE_MT_nw_merge_nodes_menu"
3863 bl_label = "Merge Selected Nodes"
3865 def draw(self, context):
3866 type = context.space_data.tree_type
3867 layout = self.layout
3868 if type == 'ShaderNodeTree':
3869 layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
3870 if type == 'GeometryNodeTree':
3871 layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes")
3872 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3873 else:
3874 layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
3875 layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
3876 props = layout.operator(NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
3877 props.mode = 'MIX'
3878 props.merge_type = 'ZCOMBINE'
3879 props = layout.operator(NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
3880 props.mode = 'MIX'
3881 props.merge_type = 'ALPHAOVER'
3883 class NWMergeGeometryMenu(Menu, NWBase):
3884 bl_idname = "NODE_MT_nw_merge_geometry_menu"
3885 bl_label = "Merge Selected Nodes using Geometry Nodes"
3886 def draw(self, context):
3887 layout = self.layout
3888 # The boolean node + Join Geometry node
3889 for type, name, description in geo_combine_operations:
3890 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3891 props.mode = type
3892 props.merge_type = 'GEOMETRY'
3894 class NWMergeShadersMenu(Menu, NWBase):
3895 bl_idname = "NODE_MT_nw_merge_shaders_menu"
3896 bl_label = "Merge Selected Nodes using Shaders"
3898 def draw(self, context):
3899 layout = self.layout
3900 for type in ('MIX', 'ADD'):
3901 props = layout.operator(NWMergeNodes.bl_idname, text=type)
3902 props.mode = type
3903 props.merge_type = 'SHADER'
3906 class NWMergeMixMenu(Menu, NWBase):
3907 bl_idname = "NODE_MT_nw_merge_mix_menu"
3908 bl_label = "Merge Selected Nodes using Mix"
3910 def draw(self, context):
3911 layout = self.layout
3912 for type, name, description in blend_types:
3913 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3914 props.mode = type
3915 props.merge_type = 'MIX'
3918 class NWConnectionListOutputs(Menu, NWBase):
3919 bl_idname = "NODE_MT_nw_connection_list_out"
3920 bl_label = "From:"
3922 def draw(self, context):
3923 layout = self.layout
3924 nodes, links = get_nodes_links(context)
3926 n1 = nodes[context.scene.NWLazySource]
3927 for index, output in enumerate(n1.outputs):
3928 # Only show sockets that are exposed.
3929 if output.enabled:
3930 layout.operator(NWCallInputsMenu.bl_idname, text=output.name, icon="RADIOBUT_OFF").from_socket=index
3933 class NWConnectionListInputs(Menu, NWBase):
3934 bl_idname = "NODE_MT_nw_connection_list_in"
3935 bl_label = "To:"
3937 def draw(self, context):
3938 layout = self.layout
3939 nodes, links = get_nodes_links(context)
3941 n2 = nodes[context.scene.NWLazyTarget]
3943 for index, input in enumerate(n2.inputs):
3944 # Only show sockets that are exposed.
3945 # This prevents, for example, the scale value socket
3946 # of the vector math node being added to the list when
3947 # the mode is not 'SCALE'.
3948 if input.enabled:
3949 op = layout.operator(NWMakeLink.bl_idname, text=input.name, icon="FORWARD")
3950 op.from_socket = context.scene.NWSourceSocket
3951 op.to_socket = index
3954 class NWMergeMathMenu(Menu, NWBase):
3955 bl_idname = "NODE_MT_nw_merge_math_menu"
3956 bl_label = "Merge Selected Nodes using Math"
3958 def draw(self, context):
3959 layout = self.layout
3960 for type, name, description in operations:
3961 props = layout.operator(NWMergeNodes.bl_idname, text=name)
3962 props.mode = type
3963 props.merge_type = 'MATH'
3966 class NWBatchChangeNodesMenu(Menu, NWBase):
3967 bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
3968 bl_label = "Batch Change Selected Nodes"
3970 def draw(self, context):
3971 layout = self.layout
3972 layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
3973 layout.menu(NWBatchChangeOperationMenu.bl_idname)
3976 class NWBatchChangeBlendTypeMenu(Menu, NWBase):
3977 bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
3978 bl_label = "Batch Change Blend Type"
3980 def draw(self, context):
3981 layout = self.layout
3982 for type, name, description in blend_types:
3983 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3984 props.blend_type = type
3985 props.operation = 'CURRENT'
3988 class NWBatchChangeOperationMenu(Menu, NWBase):
3989 bl_idname = "NODE_MT_nw_batch_change_operation_menu"
3990 bl_label = "Batch Change Math Operation"
3992 def draw(self, context):
3993 layout = self.layout
3994 for type, name, description in operations:
3995 props = layout.operator(NWBatchChangeNodes.bl_idname, text=name)
3996 props.blend_type = 'CURRENT'
3997 props.operation = type
4000 class NWCopyToSelectedMenu(Menu, NWBase):
4001 bl_idname = "NODE_MT_nw_copy_node_properties_menu"
4002 bl_label = "Copy to Selected"
4004 def draw(self, context):
4005 layout = self.layout
4006 layout.operator(NWCopySettings.bl_idname, text="Settings from Active")
4007 layout.menu(NWCopyLabelMenu.bl_idname)
4010 class NWCopyLabelMenu(Menu, NWBase):
4011 bl_idname = "NODE_MT_nw_copy_label_menu"
4012 bl_label = "Copy Label"
4014 def draw(self, context):
4015 layout = self.layout
4016 layout.operator(NWCopyLabel.bl_idname, text="from Active Node's Label").option = 'FROM_ACTIVE'
4017 layout.operator(NWCopyLabel.bl_idname, text="from Linked Node's Label").option = 'FROM_NODE'
4018 layout.operator(NWCopyLabel.bl_idname, text="from Linked Output's Name").option = 'FROM_SOCKET'
4021 class NWAddReroutesMenu(Menu, NWBase):
4022 bl_idname = "NODE_MT_nw_add_reroutes_menu"
4023 bl_label = "Add Reroutes"
4024 bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
4026 def draw(self, context):
4027 layout = self.layout
4028 layout.operator(NWAddReroutes.bl_idname, text="to All Outputs").option = 'ALL'
4029 layout.operator(NWAddReroutes.bl_idname, text="to Loose Outputs").option = 'LOOSE'
4030 layout.operator(NWAddReroutes.bl_idname, text="to Linked Outputs").option = 'LINKED'
4033 class NWLinkActiveToSelectedMenu(Menu, NWBase):
4034 bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
4035 bl_label = "Link Active to Selected"
4037 def draw(self, context):
4038 layout = self.layout
4039 layout.menu(NWLinkStandardMenu.bl_idname)
4040 layout.menu(NWLinkUseNodeNameMenu.bl_idname)
4041 layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
4044 class NWLinkStandardMenu(Menu, NWBase):
4045 bl_idname = "NODE_MT_nw_link_standard_menu"
4046 bl_label = "To All Selected"
4048 def draw(self, context):
4049 layout = self.layout
4050 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4051 props.replace = False
4052 props.use_node_name = False
4053 props.use_outputs_names = False
4054 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4055 props.replace = True
4056 props.use_node_name = False
4057 props.use_outputs_names = False
4060 class NWLinkUseNodeNameMenu(Menu, NWBase):
4061 bl_idname = "NODE_MT_nw_link_use_node_name_menu"
4062 bl_label = "Use Node Name/Label"
4064 def draw(self, context):
4065 layout = self.layout
4066 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4067 props.replace = False
4068 props.use_node_name = True
4069 props.use_outputs_names = False
4070 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4071 props.replace = True
4072 props.use_node_name = True
4073 props.use_outputs_names = False
4076 class NWLinkUseOutputsNamesMenu(Menu, NWBase):
4077 bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
4078 bl_label = "Use Outputs Names"
4080 def draw(self, context):
4081 layout = self.layout
4082 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Don't Replace Links")
4083 props.replace = False
4084 props.use_node_name = False
4085 props.use_outputs_names = True
4086 props = layout.operator(NWLinkActiveToSelected.bl_idname, text="Replace Links")
4087 props.replace = True
4088 props.use_node_name = False
4089 props.use_outputs_names = True
4092 class NWAttributeMenu(bpy.types.Menu):
4093 bl_idname = "NODE_MT_nw_node_attribute_menu"
4094 bl_label = "Attributes"
4096 @classmethod
4097 def poll(cls, context):
4098 valid = False
4099 if nw_check(context):
4100 snode = context.space_data
4101 valid = snode.tree_type == 'ShaderNodeTree'
4102 return valid
4104 def draw(self, context):
4105 l = self.layout
4106 nodes, links = get_nodes_links(context)
4107 mat = context.object.active_material
4109 objs = []
4110 for obj in bpy.data.objects:
4111 for slot in obj.material_slots:
4112 if slot.material == mat:
4113 objs.append(obj)
4114 attrs = []
4115 for obj in objs:
4116 if obj.data.attributes:
4117 for attr in obj.data.attributes:
4118 attrs.append(attr.name)
4119 attrs = list(set(attrs)) # get a unique list
4121 if attrs:
4122 for attr in attrs:
4123 l.operator(NWAddAttrNode.bl_idname, text=attr).attr_name = attr
4124 else:
4125 l.label(text="No attributes on objects with this material")
4128 class NWSwitchNodeTypeMenu(Menu, NWBase):
4129 bl_idname = "NODE_MT_nw_switch_node_type_menu"
4130 bl_label = "Switch Type to..."
4132 def draw(self, context):
4133 layout = self.layout
4134 categories = [c for c in node_categories_iter(context)
4135 if c.name not in ['Group', 'Script']]
4136 for cat in categories:
4137 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4138 if hasattr(bpy.types, idname):
4139 layout.menu(idname)
4140 else:
4141 layout.label(text="Unable to load altered node lists.")
4142 layout.label(text="Please re-enable Node Wrangler.")
4143 break
4146 def draw_switch_category_submenu(self, context):
4147 layout = self.layout
4148 if self.category.name == 'Layout':
4149 for node in self.category.items(context):
4150 if node.nodetype != 'NodeFrame':
4151 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4152 props.to_type = node.nodetype
4153 else:
4154 for node in self.category.items(context):
4155 if isinstance(node, NodeItemCustom):
4156 node.draw(self, layout, context)
4157 continue
4158 props = layout.operator(NWSwitchNodeType.bl_idname, text=node.label)
4159 props.to_type = node.nodetype
4162 # APPENDAGES TO EXISTING UI
4166 def select_parent_children_buttons(self, context):
4167 layout = self.layout
4168 layout.operator(NWSelectParentChildren.bl_idname, text="Select frame's members (children)").option = 'CHILD'
4169 layout.operator(NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
4172 def attr_nodes_menu_func(self, context):
4173 col = self.layout.column(align=True)
4174 col.menu("NODE_MT_nw_node_attribute_menu")
4175 col.separator()
4178 def multipleimages_menu_func(self, context):
4179 col = self.layout.column(align=True)
4180 col.operator(NWAddMultipleImages.bl_idname, text="Multiple Images")
4181 col.operator(NWAddSequence.bl_idname, text="Image Sequence")
4182 col.separator()
4185 def bgreset_menu_func(self, context):
4186 self.layout.operator(NWResetBG.bl_idname)
4189 def save_viewer_menu_func(self, context):
4190 if nw_check(context):
4191 if context.space_data.tree_type == 'CompositorNodeTree':
4192 if context.scene.node_tree.nodes.active:
4193 if context.scene.node_tree.nodes.active.type == "VIEWER":
4194 self.layout.operator(NWSaveViewer.bl_idname, icon='FILE_IMAGE')
4197 def reset_nodes_button(self, context):
4198 node_active = context.active_node
4199 node_selected = context.selected_nodes
4200 node_ignore = ["FRAME","REROUTE", "GROUP"]
4202 # Check if active node is in the selection and respective type
4203 if (len(node_selected) == 1) and node_active and node_active.select and node_active.type not in node_ignore:
4204 row = self.layout.row()
4205 row.operator("node.nw_reset_nodes", text="Reset Node", icon="FILE_REFRESH")
4206 self.layout.separator()
4208 elif (len(node_selected) == 1) and node_active and node_active.select and node_active.type == "FRAME":
4209 row = self.layout.row()
4210 row.operator("node.nw_reset_nodes", text="Reset Nodes in Frame", icon="FILE_REFRESH")
4211 self.layout.separator()
4215 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4217 switch_category_menus = []
4218 addon_keymaps = []
4219 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4220 # props entry: (property name, property value)
4221 kmi_defs = (
4222 # MERGE NODES
4223 # NWMergeNodes with Ctrl (AUTO).
4224 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
4225 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4226 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
4227 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4228 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4229 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4230 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
4231 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4232 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4233 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4234 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
4235 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4236 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4237 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4238 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
4239 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4240 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4241 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4242 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
4243 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4244 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
4245 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4246 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
4247 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4248 (NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4249 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4250 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4251 (NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
4252 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4253 (NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
4254 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4255 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4256 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4257 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
4258 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4259 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4260 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4261 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
4262 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4263 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4264 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4265 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
4266 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4267 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4268 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4269 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
4270 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4271 # NWMergeNodes with Ctrl Shift (MATH)
4272 (NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4273 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4274 (NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
4275 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4276 (NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4277 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4278 (NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
4279 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4280 (NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4281 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4282 (NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
4283 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4284 (NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4285 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4286 (NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
4287 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4288 (NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
4289 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4290 (NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
4291 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4292 # BATCH CHANGE NODES
4293 # NWBatchChangeNodes with Alt
4294 (NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
4295 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4296 (NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
4297 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4298 (NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4299 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4300 (NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
4301 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4302 (NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4303 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4304 (NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
4305 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4306 (NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4307 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4308 (NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
4309 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4310 (NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4311 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4312 (NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
4313 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4314 (NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
4315 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4316 (NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
4317 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4318 (NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
4319 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4320 (NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
4321 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4322 # LINK ACTIVE TO SELECTED
4323 # Don't use names, don't replace links (K)
4324 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
4325 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4326 # Don't use names, replace links (Shift K)
4327 (NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
4328 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4329 # Use node name, don't replace links (')
4330 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
4331 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4332 # Use node name, replace links (Shift ')
4333 (NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
4334 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4335 # Don't use names, don't replace links (;)
4336 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
4337 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4338 # Don't use names, replace links (')
4339 (NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
4340 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4341 # CHANGE MIX FACTOR
4342 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4343 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4344 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4345 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4346 (NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4347 (NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4348 (NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4349 (NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4350 (NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4351 (NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4352 # CLEAR LABEL (Alt L)
4353 (NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4354 # MODIFY LABEL (Alt Shift L)
4355 (NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4356 # Copy Label from active to selected
4357 (NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4358 # DETACH OUTPUTS (Alt Shift D)
4359 (NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4360 # LINK TO OUTPUT NODE (O)
4361 (NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4362 # SELECT PARENT/CHILDREN
4363 # Select Children
4364 (NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4365 # Select Parent
4366 (NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4367 # Add Texture Setup
4368 (NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4369 # Add Principled BSDF Texture Setup
4370 (NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4371 # Reset backdrop
4372 (NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4373 # Delete unused
4374 (NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4375 # Frame Selected
4376 (NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4377 # Swap Links
4378 (NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Links"),
4379 # Preview Node
4380 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
4381 (NWPreviewNode.bl_idname, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
4382 # Reload Images
4383 (NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
4384 # Lazy Mix
4385 (NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4386 # Lazy Connect
4387 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
4388 # Lazy Connect with Menu
4389 (NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4390 # Viewer Tile Center
4391 (NWViewerFocus.bl_idname, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4392 # Align Nodes
4393 (NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4394 # Reset Nodes (Back Space)
4395 (NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4396 # MENUS
4397 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
4398 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4399 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
4400 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
4401 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
4402 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
4406 classes = (
4407 NWPrincipledPreferences,
4408 NWNodeWrangler,
4409 NWLazyMix,
4410 NWLazyConnect,
4411 NWDeleteUnused,
4412 NWSwapLinks,
4413 NWResetBG,
4414 NWAddAttrNode,
4415 NWPreviewNode,
4416 NWFrameSelected,
4417 NWReloadImages,
4418 NWSwitchNodeType,
4419 NWMergeNodes,
4420 NWBatchChangeNodes,
4421 NWChangeMixFactor,
4422 NWCopySettings,
4423 NWCopyLabel,
4424 NWClearLabel,
4425 NWModifyLabels,
4426 NWAddTextureSetup,
4427 NWAddPrincipledSetup,
4428 NWAddReroutes,
4429 NWLinkActiveToSelected,
4430 NWAlignNodes,
4431 NWSelectParentChildren,
4432 NWDetachOutputs,
4433 NWLinkToOutputNode,
4434 NWMakeLink,
4435 NWCallInputsMenu,
4436 NWAddSequence,
4437 NWAddMultipleImages,
4438 NWViewerFocus,
4439 NWSaveViewer,
4440 NWResetNodes,
4441 NodeWranglerPanel,
4442 NodeWranglerMenu,
4443 NWMergeNodesMenu,
4444 NWMergeShadersMenu,
4445 NWMergeGeometryMenu,
4446 NWMergeMixMenu,
4447 NWConnectionListOutputs,
4448 NWConnectionListInputs,
4449 NWMergeMathMenu,
4450 NWBatchChangeNodesMenu,
4451 NWBatchChangeBlendTypeMenu,
4452 NWBatchChangeOperationMenu,
4453 NWCopyToSelectedMenu,
4454 NWCopyLabelMenu,
4455 NWAddReroutesMenu,
4456 NWLinkActiveToSelectedMenu,
4457 NWLinkStandardMenu,
4458 NWLinkUseNodeNameMenu,
4459 NWLinkUseOutputsNamesMenu,
4460 NWAttributeMenu,
4461 NWSwitchNodeTypeMenu,
4464 def register():
4465 from bpy.utils import register_class
4467 # props
4468 bpy.types.Scene.NWBusyDrawing = StringProperty(
4469 name="Busy Drawing!",
4470 default="",
4471 description="An internal property used to store only the first mouse position")
4472 bpy.types.Scene.NWLazySource = StringProperty(
4473 name="Lazy Source!",
4474 default="x",
4475 description="An internal property used to store the first node in a Lazy Connect operation")
4476 bpy.types.Scene.NWLazyTarget = StringProperty(
4477 name="Lazy Target!",
4478 default="x",
4479 description="An internal property used to store the last node in a Lazy Connect operation")
4480 bpy.types.Scene.NWSourceSocket = IntProperty(
4481 name="Source Socket!",
4482 default=0,
4483 description="An internal property used to store the source socket in a Lazy Connect operation")
4484 bpy.types.NodeSocketInterface.NWViewerSocket = BoolProperty(
4485 name="NW Socket",
4486 default=False,
4487 description="An internal property used to determine if a socket is generated by the addon"
4490 for cls in classes:
4491 register_class(cls)
4493 # keymaps
4494 addon_keymaps.clear()
4495 kc = bpy.context.window_manager.keyconfigs.addon
4496 if kc:
4497 km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
4498 for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
4499 kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
4500 if props:
4501 for prop, value in props:
4502 setattr(kmi.properties, prop, value)
4503 addon_keymaps.append((km, kmi))
4505 # menu items
4506 bpy.types.NODE_MT_select.append(select_parent_children_buttons)
4507 bpy.types.NODE_MT_category_SH_NEW_INPUT.prepend(attr_nodes_menu_func)
4508 bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
4509 bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
4510 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.prepend(multipleimages_menu_func)
4511 bpy.types.NODE_MT_category_CMP_INPUT.prepend(multipleimages_menu_func)
4512 bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
4513 bpy.types.NODE_MT_node.prepend(reset_nodes_button)
4515 # switch submenus
4516 switch_category_menus.clear()
4517 for cat in node_categories_iter(None):
4518 if cat.name not in ['Group', 'Script']:
4519 idname = f"NODE_MT_nw_switch_{cat.identifier}_submenu"
4520 switch_category_type = type(idname, (bpy.types.Menu,), {
4521 "bl_space_type": 'NODE_EDITOR',
4522 "bl_label": cat.name,
4523 "category": cat,
4524 "poll": cat.poll,
4525 "draw": draw_switch_category_submenu,
4528 switch_category_menus.append(switch_category_type)
4530 bpy.utils.register_class(switch_category_type)
4533 def unregister():
4534 from bpy.utils import unregister_class
4536 # props
4537 del bpy.types.Scene.NWBusyDrawing
4538 del bpy.types.Scene.NWLazySource
4539 del bpy.types.Scene.NWLazyTarget
4540 del bpy.types.Scene.NWSourceSocket
4541 del bpy.types.NodeSocketInterface.NWViewerSocket
4543 for cat_types in switch_category_menus:
4544 bpy.utils.unregister_class(cat_types)
4545 switch_category_menus.clear()
4547 # keymaps
4548 for km, kmi in addon_keymaps:
4549 km.keymap_items.remove(kmi)
4550 addon_keymaps.clear()
4552 # menuitems
4553 bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
4554 bpy.types.NODE_MT_category_SH_NEW_INPUT.remove(attr_nodes_menu_func)
4555 bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
4556 bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
4557 bpy.types.NODE_MT_category_SH_NEW_TEXTURE.remove(multipleimages_menu_func)
4558 bpy.types.NODE_MT_category_CMP_INPUT.remove(multipleimages_menu_func)
4559 bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
4560 bpy.types.NODE_MT_node.remove(reset_nodes_button)
4562 for cls in classes:
4563 unregister_class(cls)
4565 if __name__ == "__main__":
4566 register()