1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Node Wrangler",
5 "author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
8 "location": "Node Editor Toolbar or Shift-W",
9 "description": "Various tools to enhance and speed up node-based workflow",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
17 from bpy
.types
import Operator
, Panel
, Menu
18 from bpy
.props
import (
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
35 from itertools
import chain
37 from collections
import namedtuple
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'])
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.
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.
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.
154 ('CURRENT', 'Current', 'Leave at current state'),
155 ('NEXT', 'Next', 'Next blend type/operation'),
156 ('PREV', 'Prev', 'Previous blend type/operation'),
161 (1.0, 1.0, 1.0, 0.7),
162 (1.0, 0.0, 0.0, 0.7),
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)
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)
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)
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)
186 (1.0, 1.0, 1.0, 0.7),
187 (0.0, 0.0, 0.0, 0.7),
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
:
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
213 'MIDDLEMOUSE': "MMB",
215 'WHEELUPMOUSE': "Wheel Up",
216 'WHEELDOWNMOUSE': "Wheel Down",
217 'WHEELINMOUSE': "Wheel In",
218 'WHEELOUTMOUSE': "Wheel Out",
231 'LINE_FEED': "Enter",
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 +",
258 return nice_name
[punc
]
260 return punc
.replace("_", " ").title()
263 def force_update(context
):
264 context
.space_data
.node_tree
.update_tag()
268 prefs
= bpy
.context
.preferences
.system
269 return prefs
.dpi
* prefs
.pixel_size
/ 72
272 def node_mid_pt(node
, axis
):
274 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
276 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
282 def autolink(node1
, node2
, links
):
284 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
285 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
286 for outp
in available_outputs
:
287 for inp
in available_inputs
:
288 if not inp
.is_linked
and inp
.name
== outp
.name
:
293 for outp
in available_outputs
:
294 for inp
in available_inputs
:
295 if not inp
.is_linked
and inp
.type == outp
.type:
300 # force some connection even if the type doesn't match
301 if available_outputs
:
302 for inp
in available_inputs
:
303 if not inp
.is_linked
:
305 links
.new(available_outputs
[0], inp
)
308 # even if no sockets are open, force one of matching type
309 for outp
in available_outputs
:
310 for inp
in available_inputs
:
311 if inp
.type == outp
.type:
317 for outp
in available_outputs
:
318 for inp
in available_inputs
:
323 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
326 def abs_node_location(node
):
327 abs_location
= node
.location
328 if node
.parent
is None:
330 return abs_location
+ abs_node_location(node
.parent
)
332 def node_at_pos(nodes
, context
, event
):
333 nodes_under_mouse
= []
336 store_mouse_cursor(context
, event
)
337 x
, y
= context
.space_data
.cursor_location
339 # Make a list of each corner (and middle of border) for each node.
340 # Will be sorted to find nearest point and thus nearest node
341 node_points_with_dist
= []
344 if node
.type != 'FRAME': # no point trying to link to a frame node
345 dimx
= node
.dimensions
.x
/dpifac()
346 dimy
= node
.dimensions
.y
/dpifac()
347 locx
, locy
= abs_node_location(node
)
350 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
351 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
352 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
353 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
355 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
356 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
357 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
358 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
360 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
363 if node
.type != 'FRAME' and skipnode
== False:
364 locx
, locy
= abs_node_location(node
)
365 dimx
= node
.dimensions
.x
/dpifac()
366 dimy
= node
.dimensions
.y
/dpifac()
367 if (locx
<= x
<= locx
+ dimx
) and \
368 (locy
- dimy
<= y
<= locy
):
369 nodes_under_mouse
.append(node
)
371 if len(nodes_under_mouse
) == 1:
372 if nodes_under_mouse
[0] != nearest_node
:
373 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
375 target_node
= nearest_node
# else use the nearest node
377 target_node
= nearest_node
381 def store_mouse_cursor(context
, event
):
382 space
= context
.space_data
383 v2d
= context
.region
.view2d
384 tree
= space
.edit_tree
386 # convert mouse position to the View2D for later node placement
387 if context
.region
.type == 'WINDOW':
388 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
390 space
.cursor_location
= tree
.view_center
392 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
393 shader
= gpu
.shader
.from_builtin('2D_SMOOTH_COLOR')
395 vertices
= ((x1
, y1
), (x2
, y2
))
396 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
397 colour
[1]+(1.0-colour
[1])/4,
398 colour
[2]+(1.0-colour
[2])/4,
399 colour
[3]+(1.0-colour
[3])/4),
402 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
403 bgl
.glLineWidth(size
* dpifac())
409 def draw_circle_2d_filled(shader
, mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
410 radius
= radius
* dpifac()
412 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
413 radius
* sin(i
* 2 * pi
/ sides
) + my
)
414 for i
in range(sides
+ 1)]
416 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
418 shader
.uniform_float("color", colour
)
422 def draw_rounded_node_border(shader
, node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
423 area_width
= bpy
.context
.area
.width
425 radius
= radius
*dpifac()
427 nlocx
, nlocy
= abs_node_location(node
)
429 nlocx
= (nlocx
+1)*dpifac()
430 nlocy
= (nlocy
+1)*dpifac()
431 ndimx
= node
.dimensions
.x
432 ndimy
= node
.dimensions
.y
437 if node
.type == 'REROUTE':
445 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
447 for i
in range(sides
+1):
450 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
451 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
452 vertices
.append((cosine
,sine
))
453 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
455 shader
.uniform_float("color", colour
)
459 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
461 for i
in range(sides
+1):
464 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
465 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
466 vertices
.append((cosine
,sine
))
467 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
469 shader
.uniform_float("color", colour
)
473 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
475 for i
in range(sides
+1):
478 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
479 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
480 vertices
.append((cosine
,sine
))
481 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
483 shader
.uniform_float("color", colour
)
486 # Bottom right corner
487 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
489 for i
in range(sides
+1):
492 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
493 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
494 vertices
.append((cosine
,sine
))
495 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
497 shader
.uniform_float("color", colour
)
500 # prepare drawing all edges in one batch
506 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
507 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
508 if m1x
< area_width
and m2x
< area_width
:
509 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
510 (m1x
,m1y
), (m1x
-radius
,m1y
)])
511 indices
.extend([(id_last
, id_last
+1, id_last
+3),
512 (id_last
+3, id_last
+1, id_last
+2)])
516 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
517 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
518 m1x
= min(m1x
, area_width
)
519 m2x
= min(m2x
, area_width
)
520 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
521 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
522 indices
.extend([(id_last
, id_last
+1, id_last
+3),
523 (id_last
+3, id_last
+1, id_last
+2)])
527 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
528 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
529 if m1x
< area_width
and m2x
< area_width
:
530 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
531 (m1x
+radius
,m1y
), (m1x
,m1y
)])
532 indices
.extend([(id_last
, id_last
+1, id_last
+3),
533 (id_last
+3, id_last
+1, id_last
+2)])
537 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
538 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
539 m1x
= min(m1x
, area_width
)
540 m2x
= min(m2x
, area_width
)
541 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
542 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
543 indices
.extend([(id_last
, id_last
+1, id_last
+3),
544 (id_last
+3, id_last
+1, id_last
+2)])
546 # now draw all edges in one batch
547 if len(vertices
) != 0:
548 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
550 shader
.uniform_float("color", colour
)
553 def draw_callback_nodeoutline(self
, context
, mode
):
557 bgl
.glEnable(bgl
.GL_BLEND
)
558 bgl
.glEnable(bgl
.GL_LINE_SMOOTH
)
559 bgl
.glHint(bgl
.GL_LINE_SMOOTH_HINT
, bgl
.GL_NICEST
)
561 nodes
, links
= get_nodes_links(context
)
563 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR')
566 col_outer
= (1.0, 0.2, 0.2, 0.4)
567 col_inner
= (0.0, 0.0, 0.0, 0.5)
568 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
569 elif mode
== "LINKMENU":
570 col_outer
= (0.4, 0.6, 1.0, 0.4)
571 col_inner
= (0.0, 0.0, 0.0, 0.5)
572 col_circle_inner
= (0.08, 0.15, .3, 1.0)
574 col_outer
= (0.2, 1.0, 0.2, 0.4)
575 col_inner
= (0.0, 0.0, 0.0, 0.5)
576 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
578 m1x
= self
.mouse_path
[0][0]
579 m1y
= self
.mouse_path
[0][1]
580 m2x
= self
.mouse_path
[-1][0]
581 m2y
= self
.mouse_path
[-1][1]
583 n1
= nodes
[context
.scene
.NWLazySource
]
584 n2
= nodes
[context
.scene
.NWLazyTarget
]
587 col_outer
= (0.4, 0.4, 0.4, 0.4)
588 col_inner
= (0.0, 0.0, 0.0, 0.5)
589 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
591 draw_rounded_node_border(shader
, n1
, radius
=6, colour
=col_outer
) # outline
592 draw_rounded_node_border(shader
, n1
, radius
=5, colour
=col_inner
) # inner
593 draw_rounded_node_border(shader
, n2
, radius
=6, colour
=col_outer
) # outline
594 draw_rounded_node_border(shader
, n2
, radius
=5, colour
=col_inner
) # inner
596 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
597 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
600 draw_circle_2d_filled(shader
, m1x
, m1y
, 7, col_outer
)
601 draw_circle_2d_filled(shader
, m2x
, m2y
, 7, col_outer
)
604 draw_circle_2d_filled(shader
, m1x
, m1y
, 5, col_circle_inner
)
605 draw_circle_2d_filled(shader
, m2x
, m2y
, 5, col_circle_inner
)
607 bgl
.glDisable(bgl
.GL_BLEND
)
608 bgl
.glDisable(bgl
.GL_LINE_SMOOTH
)
609 def get_active_tree(context
):
610 tree
= context
.space_data
.node_tree
612 # Get nodes from currently edited tree.
613 # If user is editing a group, space_data.node_tree is still the base level (outside group).
614 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
615 # the same as context.active_node, the user is in a group.
616 # Check recursively until we find the real active node_tree:
617 if tree
.nodes
.active
:
618 while tree
.nodes
.active
!= context
.active_node
:
619 tree
= tree
.nodes
.active
.node_tree
623 def get_nodes_links(context
):
624 tree
, path
= get_active_tree(context
)
625 return tree
.nodes
, tree
.links
627 def is_viewer_socket(socket
):
628 # checks if a internal socket is a valid viewer socket
629 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
631 def get_internal_socket(socket
):
632 #get the internal socket from a socket inside or outside the group
634 if node
.type == 'GROUP_OUTPUT':
635 source_iterator
= node
.inputs
636 iterator
= node
.id_data
.outputs
637 elif node
.type == 'GROUP_INPUT':
638 source_iterator
= node
.outputs
639 iterator
= node
.id_data
.inputs
640 elif hasattr(node
, "node_tree"):
642 source_iterator
= node
.outputs
643 iterator
= node
.node_tree
.outputs
645 source_iterator
= node
.inputs
646 iterator
= node
.node_tree
.inputs
650 for i
, s
in enumerate(source_iterator
):
655 def is_viewer_link(link
, output_node
):
656 if link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
658 if link
.to_node
.type == 'GROUP_OUTPUT':
659 socket
= get_internal_socket(link
.to_socket
)
660 if is_viewer_socket(socket
):
664 def get_group_output_node(tree
):
665 for node
in tree
.nodes
:
666 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
669 def get_output_location(tree
):
670 # get right-most location
671 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
672 max_xloc_node
= sorted_by_xloc
[-1]
674 # get average y location
676 for node
in tree
.nodes
:
677 sum_yloc
+= node
.location
.y
679 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
680 loc_y
= sum_yloc
/ len(tree
.nodes
)
684 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
685 base_color
: StringProperty(
687 default
='diffuse diff albedo base col color',
688 description
='Naming Components for Base Color maps')
689 sss_color
: StringProperty(
690 name
='Subsurface Color',
691 default
='sss subsurface',
692 description
='Naming Components for Subsurface Color maps')
693 metallic
: StringProperty(
695 default
='metallic metalness metal mtl',
696 description
='Naming Components for metallness maps')
697 specular
: StringProperty(
699 default
='specularity specular spec spc',
700 description
='Naming Components for Specular maps')
701 normal
: StringProperty(
703 default
='normal nor nrm nrml norm',
704 description
='Naming Components for Normal maps')
705 bump
: StringProperty(
708 description
='Naming Components for bump maps')
709 rough
: StringProperty(
711 default
='roughness rough rgh',
712 description
='Naming Components for roughness maps')
713 gloss
: StringProperty(
715 default
='gloss glossy glossiness',
716 description
='Naming Components for glossy maps')
717 displacement
: StringProperty(
719 default
='displacement displace disp dsp height heightmap',
720 description
='Naming Components for displacement maps')
721 transmission
: StringProperty(
723 default
='transmission transparency',
724 description
='Naming Components for transmission maps')
725 emission
: StringProperty(
727 default
='emission emissive emit',
728 description
='Naming Components for emission maps')
729 alpha
: StringProperty(
731 default
='alpha opacity',
732 description
='Naming Components for alpha maps')
733 ambient_occlusion
: StringProperty(
734 name
='Ambient Occlusion',
735 default
='ao ambient occlusion',
736 description
='Naming Components for AO maps')
739 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
742 merge_hide
: EnumProperty(
743 name
="Hide Mix nodes",
745 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
746 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
747 ("NEVER", "Never", "Never collapse the new merge nodes")
749 default
='NON_SHADER',
750 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
751 merge_position
: EnumProperty(
752 name
="Mix Node Position",
754 ("CENTER", "Center", "Place the Mix node between the two nodes"),
755 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
758 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
760 show_hotkey_list
: BoolProperty(
761 name
="Show Hotkey List",
763 description
="Expand this box into a list of all the hotkeys for functions in this addon"
765 hotkey_list_filter
: StringProperty(
766 name
=" Filter by Name",
768 description
="Show only hotkeys that have this text in their name",
769 options
={'TEXTEDIT_UPDATE'}
771 show_principled_lists
: BoolProperty(
772 name
="Show Principled naming tags",
774 description
="Expand this box into a list of all naming tags for principled texture setup"
776 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
778 def draw(self
, context
):
780 col
= layout
.column()
781 col
.prop(self
, "merge_position")
782 col
.prop(self
, "merge_hide")
785 col
= box
.column(align
=True)
786 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
787 if self
.show_principled_lists
:
788 tags
= self
.principled_tags
790 col
.prop(tags
, "base_color")
791 col
.prop(tags
, "sss_color")
792 col
.prop(tags
, "metallic")
793 col
.prop(tags
, "specular")
794 col
.prop(tags
, "rough")
795 col
.prop(tags
, "gloss")
796 col
.prop(tags
, "normal")
797 col
.prop(tags
, "bump")
798 col
.prop(tags
, "displacement")
799 col
.prop(tags
, "transmission")
800 col
.prop(tags
, "emission")
801 col
.prop(tags
, "alpha")
802 col
.prop(tags
, "ambient_occlusion")
805 col
= box
.column(align
=True)
806 hotkey_button_name
= "Show Hotkey List"
807 if self
.show_hotkey_list
:
808 hotkey_button_name
= "Hide Hotkey List"
809 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
810 if self
.show_hotkey_list
:
811 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
813 for hotkey
in kmi_defs
:
815 hotkey_name
= hotkey
[7]
817 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
818 row
= col
.row(align
=True)
819 row
.label(text
=hotkey_name
)
820 keystr
= nice_hotkey_name(hotkey
[1])
822 keystr
= "Shift " + keystr
824 keystr
= "Alt " + keystr
826 keystr
= "Ctrl " + keystr
827 row
.label(text
=keystr
)
831 def nw_check(context
):
832 space
= context
.space_data
833 editor_is_valid
= space
.type == 'NODE_EDITOR'
835 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
836 tree_is_valid
= space
.node_tree
is not None and space
.tree_type
in valid_trees
838 return editor_is_valid
and tree_is_valid
842 def poll(cls
, context
):
843 return nw_check(context
)
847 class NWLazyMix(Operator
, NWBase
):
848 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
849 bl_idname
= "node.nw_lazy_mix"
850 bl_label
= "Mix Nodes"
851 bl_options
= {'REGISTER', 'UNDO'}
853 def modal(self
, context
, event
):
854 context
.area
.tag_redraw()
855 nodes
, links
= get_nodes_links(context
)
858 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
861 if not context
.scene
.NWBusyDrawing
:
862 node1
= node_at_pos(nodes
, context
, event
)
864 context
.scene
.NWBusyDrawing
= node1
.name
866 if context
.scene
.NWBusyDrawing
!= 'STOP':
867 node1
= nodes
[context
.scene
.NWBusyDrawing
]
869 context
.scene
.NWLazySource
= node1
.name
870 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
872 if event
.type == 'MOUSEMOVE':
873 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
875 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
876 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
877 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
880 node2
= node_at_pos(nodes
, context
, event
)
882 context
.scene
.NWBusyDrawing
= node2
.name
894 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
896 context
.scene
.NWBusyDrawing
= ""
899 elif event
.type == 'ESC':
901 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
904 return {'RUNNING_MODAL'}
906 def invoke(self
, context
, event
):
907 if context
.area
.type == 'NODE_EDITOR':
908 # the arguments we pass the the callback
909 args
= (self
, context
, 'MIX')
910 # Add the region OpenGL drawing callback
911 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
912 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
916 context
.window_manager
.modal_handler_add(self
)
917 return {'RUNNING_MODAL'}
919 self
.report({'WARNING'}, "View3D not found, cannot run operator")
923 class NWLazyConnect(Operator
, NWBase
):
924 """Connect two nodes without clicking a specific socket (automatically determined"""
925 bl_idname
= "node.nw_lazy_connect"
926 bl_label
= "Lazy Connect"
927 bl_options
= {'REGISTER', 'UNDO'}
928 with_menu
: BoolProperty()
930 def modal(self
, context
, event
):
931 context
.area
.tag_redraw()
932 nodes
, links
= get_nodes_links(context
)
935 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
938 if not context
.scene
.NWBusyDrawing
:
939 node1
= node_at_pos(nodes
, context
, event
)
941 context
.scene
.NWBusyDrawing
= node1
.name
943 if context
.scene
.NWBusyDrawing
!= 'STOP':
944 node1
= nodes
[context
.scene
.NWBusyDrawing
]
946 context
.scene
.NWLazySource
= node1
.name
947 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
949 if event
.type == 'MOUSEMOVE':
950 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
952 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
953 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
954 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
957 node2
= node_at_pos(nodes
, context
, event
)
959 context
.scene
.NWBusyDrawing
= node2
.name
970 if node
.select
== True:
972 original_sel
.append(node
)
974 original_unsel
.append(node
)
978 #link_success = autolink(node1, node2, links)
980 if len(node1
.outputs
) > 1 and node2
.inputs
:
981 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
982 elif len(node1
.outputs
) == 1:
983 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
985 link_success
= autolink(node1
, node2
, links
)
987 for node
in original_sel
:
989 for node
in original_unsel
:
993 force_update(context
)
994 context
.scene
.NWBusyDrawing
= ""
997 elif event
.type == 'ESC':
998 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
1001 return {'RUNNING_MODAL'}
1003 def invoke(self
, context
, event
):
1004 if context
.area
.type == 'NODE_EDITOR':
1005 nodes
, links
= get_nodes_links(context
)
1006 node
= node_at_pos(nodes
, context
, event
)
1008 context
.scene
.NWBusyDrawing
= node
.name
1010 # the arguments we pass the the callback
1014 args
= (self
, context
, mode
)
1015 # Add the region OpenGL drawing callback
1016 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1017 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1019 self
.mouse_path
= []
1021 context
.window_manager
.modal_handler_add(self
)
1022 return {'RUNNING_MODAL'}
1024 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1025 return {'CANCELLED'}
1028 class NWDeleteUnused(Operator
, NWBase
):
1029 """Delete all nodes whose output is not used"""
1030 bl_idname
= 'node.nw_del_unused'
1031 bl_label
= 'Delete Unused Nodes'
1032 bl_options
= {'REGISTER', 'UNDO'}
1034 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1035 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1037 def is_unused_node(self
, node
):
1038 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1039 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1040 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1041 if node
.type in end_types
:
1044 for output
in node
.outputs
:
1050 def poll(cls
, context
):
1052 if nw_check(context
):
1053 if context
.space_data
.node_tree
.nodes
:
1057 def execute(self
, context
):
1058 nodes
, links
= get_nodes_links(context
)
1063 if node
.select
== True:
1064 selection
.append(node
.name
)
1070 temp_deleted_nodes
= []
1071 del_unused_iterations
= len(nodes
)
1072 for it
in range(0, del_unused_iterations
):
1073 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1075 if self
.is_unused_node(node
):
1077 deleted_nodes
.append(node
.name
)
1078 bpy
.ops
.node
.delete()
1080 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1083 if self
.delete_frames
:
1091 frames_in_use
.append(node
.parent
)
1093 if node
.type == 'FRAME' and node
not in frames_in_use
:
1096 repeat
= True # repeat for nested frames
1098 if node
not in frames_in_use
:
1100 deleted_nodes
.append(node
.name
)
1101 bpy
.ops
.node
.delete()
1103 if self
.delete_muted
:
1107 deleted_nodes
.append(node
.name
)
1108 bpy
.ops
.node
.delete_reconnect()
1110 # get unique list of deleted nodes (iterations would count the same node more than once)
1111 deleted_nodes
= list(set(deleted_nodes
))
1112 for n
in deleted_nodes
:
1113 self
.report({'INFO'}, "Node " + n
+ " deleted")
1114 num_deleted
= len(deleted_nodes
)
1119 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1121 self
.report({'INFO'}, "Nothing deleted")
1124 nodes
, links
= get_nodes_links(context
)
1126 if node
.name
in selection
:
1130 def invoke(self
, context
, event
):
1131 return context
.window_manager
.invoke_confirm(self
, event
)
1134 class NWSwapLinks(Operator
, NWBase
):
1135 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1136 bl_idname
= 'node.nw_swap_links'
1137 bl_label
= 'Swap Links'
1138 bl_options
= {'REGISTER', 'UNDO'}
1141 def poll(cls
, context
):
1143 if nw_check(context
):
1144 if context
.selected_nodes
:
1145 valid
= len(context
.selected_nodes
) <= 2
1148 def execute(self
, context
):
1149 nodes
, links
= get_nodes_links(context
)
1150 selected_nodes
= context
.selected_nodes
1151 n1
= selected_nodes
[0]
1154 if len(selected_nodes
) == 2:
1155 n2
= selected_nodes
[1]
1156 if n1
.outputs
and n2
.outputs
:
1161 for output
in n1
.outputs
:
1163 for link
in output
.links
:
1164 n1_outputs
.append([out_index
, link
.to_socket
])
1169 for output
in n2
.outputs
:
1171 for link
in output
.links
:
1172 n2_outputs
.append([out_index
, link
.to_socket
])
1176 for connection
in n1_outputs
:
1178 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1180 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1181 for connection
in n2_outputs
:
1183 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1185 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1187 if n1
.outputs
or n2
.outputs
:
1188 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1190 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1193 elif len(selected_nodes
) == 1:
1194 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1195 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1200 for i1
in n1
.inputs
:
1201 if i1
.is_linked
and not i1
.is_multi_input
:
1203 for i2
in n1
.inputs
:
1204 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1206 types
.append ([i1
, similar_types
, i
])
1208 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1213 for i2
in n1
.inputs
:
1214 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1216 i1f
= pair
[0].links
[0].from_socket
1217 i1t
= pair
[0].links
[0].to_socket
1218 i2f
= pair
[1].links
[0].from_socket
1219 i2t
= pair
[1].links
[0].to_socket
1224 fs
= t
[0].links
[0].from_socket
1226 links
.remove(t
[0].links
[0])
1227 if i
+1 == len(n1
.inputs
):
1230 while n1
.inputs
[i
].is_linked
:
1232 links
.new(fs
, n1
.inputs
[i
])
1233 elif len(types
) == 2:
1234 i1f
= types
[0][0].links
[0].from_socket
1235 i1t
= types
[0][0].links
[0].to_socket
1236 i2f
= types
[1][0].links
[0].from_socket
1237 i2t
= types
[1][0].links
[0].to_socket
1242 self
.report({'WARNING'}, "This node has no input connections to swap!")
1244 self
.report({'WARNING'}, "This node has no inputs to swap!")
1246 force_update(context
)
1250 class NWResetBG(Operator
, NWBase
):
1251 """Reset the zoom and position of the background image"""
1252 bl_idname
= 'node.nw_bg_reset'
1253 bl_label
= 'Reset Backdrop'
1254 bl_options
= {'REGISTER', 'UNDO'}
1257 def poll(cls
, context
):
1259 if nw_check(context
):
1260 snode
= context
.space_data
1261 valid
= snode
.tree_type
== 'CompositorNodeTree'
1264 def execute(self
, context
):
1265 context
.space_data
.backdrop_zoom
= 1
1266 context
.space_data
.backdrop_offset
[0] = 0
1267 context
.space_data
.backdrop_offset
[1] = 0
1271 class NWAddAttrNode(Operator
, NWBase
):
1272 """Add an Attribute node with this name"""
1273 bl_idname
= 'node.nw_add_attr_node'
1274 bl_label
= 'Add UV map'
1275 bl_options
= {'REGISTER', 'UNDO'}
1277 attr_name
: StringProperty()
1279 def execute(self
, context
):
1280 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1281 nodes
, links
= get_nodes_links(context
)
1282 nodes
.active
.attribute_name
= self
.attr_name
1285 class NWPreviewNode(Operator
, NWBase
):
1286 bl_idname
= "node.nw_preview_node"
1287 bl_label
= "Preview Node"
1288 bl_description
= "Connect active node to the Node Group output or the Material Output"
1289 bl_options
= {'REGISTER', 'UNDO'}
1291 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1292 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1293 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1294 run_in_geometry_nodes
: BoolProperty(default
=True)
1297 self
.shader_output_type
= ""
1298 self
.shader_output_ident
= ""
1301 def poll(cls
, context
):
1302 if nw_check(context
):
1303 space
= context
.space_data
1304 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1305 if context
.active_node
:
1306 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1312 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1313 #check if a viewer output already exists in a node group otherwise create
1314 if hasattr(node
, "node_tree"):
1316 if len(node
.node_tree
.outputs
):
1318 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1319 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1320 #if viewer output is already used but leads to the same socket we can still use it
1321 is_used
= self
.is_socket_used_other_mats(socket
)
1323 if connect_socket
== None:
1325 groupout
= get_group_output_node(node
.node_tree
)
1326 groupout_input
= groupout
.inputs
[i
]
1327 links
= groupout_input
.links
1328 if connect_socket
not in [link
.from_socket
for link
in links
]:
1334 if not index
and free_socket
:
1338 #create viewer socket
1339 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1340 index
= len(node
.node_tree
.outputs
) - 1
1341 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1344 def init_shader_variables(self
, space
, shader_type
):
1345 if shader_type
== 'OBJECT':
1346 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1347 self
.shader_output_type
= "OUTPUT_MATERIAL"
1348 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1350 self
.shader_output_type
= "OUTPUT_LIGHT"
1351 self
.shader_output_ident
= "ShaderNodeOutputLight"
1353 elif shader_type
== 'WORLD':
1354 self
.shader_output_type
= "OUTPUT_WORLD"
1355 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1357 def get_shader_output_node(self
, tree
):
1358 for node
in tree
.nodes
:
1359 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1363 def ensure_group_output(cls
, tree
):
1364 #check if a group output node exists otherwise create
1365 groupout
= get_group_output_node(tree
)
1367 groupout
= tree
.nodes
.new('NodeGroupOutput')
1368 loc_x
, loc_y
= get_output_location(tree
)
1369 groupout
.location
.x
= loc_x
1370 groupout
.location
.y
= loc_y
1371 groupout
.select
= False
1372 # So that we don't keep on adding new group outputs
1373 groupout
.is_active_output
= True
1377 def search_sockets(cls
, node
, sockets
, index
=None):
1378 # recursively scan nodes for viewer sockets and store in list
1379 for i
, input_socket
in enumerate(node
.inputs
):
1380 if index
and i
!= index
:
1382 if len(input_socket
.links
):
1383 link
= input_socket
.links
[0]
1384 next_node
= link
.from_node
1385 external_socket
= link
.from_socket
1386 if hasattr(next_node
, "node_tree"):
1387 for socket_index
, s
in enumerate(next_node
.outputs
):
1388 if s
== external_socket
:
1390 socket
= next_node
.node_tree
.outputs
[socket_index
]
1391 if is_viewer_socket(socket
) and socket
not in sockets
:
1392 sockets
.append(socket
)
1393 #continue search inside of node group but restrict socket to where we came from
1394 groupout
= get_group_output_node(next_node
.node_tree
)
1395 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1398 def scan_nodes(cls
, tree
, sockets
):
1399 # get all viewer sockets in a material tree
1400 for node
in tree
.nodes
:
1401 if hasattr(node
, "node_tree"):
1402 for socket
in node
.node_tree
.outputs
:
1403 if is_viewer_socket(socket
) and (socket
not in sockets
):
1404 sockets
.append(socket
)
1405 cls
.scan_nodes(node
.node_tree
, sockets
)
1407 def link_leads_to_used_socket(self
, link
):
1408 #return True if link leads to a socket that is already used in this material
1409 socket
= get_internal_socket(link
.to_socket
)
1410 return (socket
and self
.is_socket_used_active_mat(socket
))
1412 def is_socket_used_active_mat(self
, socket
):
1413 #ensure used sockets in active material is calculated and check given socket
1414 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1415 self
.used_viewer_sockets_active_mat
= []
1416 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1418 self
.search_sockets(materialout
, self
.used_viewer_sockets_active_mat
)
1419 return socket
in self
.used_viewer_sockets_active_mat
1421 def is_socket_used_other_mats(self
, socket
):
1422 #ensure used sockets in other materials are calculated and check given socket
1423 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1424 self
.used_viewer_sockets_other_mats
= []
1425 for mat
in bpy
.data
.materials
:
1426 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1429 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1431 self
.search_sockets(materialout
, self
.used_viewer_sockets_other_mats
)
1432 return socket
in self
.used_viewer_sockets_other_mats
1434 def invoke(self
, context
, event
):
1435 space
= context
.space_data
1436 # Ignore operator when running in wrong context.
1437 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1438 return {'PASS_THROUGH'}
1440 shader_type
= space
.shader_type
1441 self
.init_shader_variables(space
, shader_type
)
1442 mlocx
= event
.mouse_region_x
1443 mlocy
= event
.mouse_region_y
1444 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
1445 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1446 active_tree
, path_to_tree
= get_active_tree(context
)
1447 nodes
, links
= active_tree
.nodes
, active_tree
.links
1448 base_node_tree
= space
.node_tree
1449 active
= nodes
.active
1451 # For geometry node trees we just connect to the group output
1452 if space
.tree_type
== "GeometryNodeTree":
1455 for out
in active
.outputs
:
1456 if is_visible_socket(out
):
1465 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1466 self
.scan_nodes(base_node_tree
, delete_sockets
)
1468 # Find (or create if needed) the output of this node tree
1469 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1471 # Analyze outputs, make links
1474 for i
, out
in enumerate(active
.outputs
):
1475 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1476 valid_outputs
.append(i
)
1478 out_i
= valid_outputs
[0] # Start index of node's outputs
1479 for i
, valid_i
in enumerate(valid_outputs
):
1480 for out_link
in active
.outputs
[valid_i
].links
:
1481 if is_viewer_link(out_link
, geometryoutput
):
1482 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1483 if i
< len(valid_outputs
) - 1:
1484 out_i
= valid_outputs
[i
+ 1]
1486 out_i
= valid_outputs
[0]
1488 make_links
= [] # store sockets for new links
1490 # If there is no 'GEOMETRY' output type - We can't preview the node
1493 socket_type
= 'GEOMETRY'
1494 # Find an input socket of the output of type geometry
1495 geometryoutindex
= None
1496 for i
,inp
in enumerate(geometryoutput
.inputs
):
1497 if inp
.type == socket_type
:
1498 geometryoutindex
= i
1500 if geometryoutindex
is None:
1501 # Create geometry socket
1502 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1503 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1505 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1506 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1507 for li_from
, li_to
in make_links
:
1508 base_node_tree
.links
.new(li_from
, li_to
)
1509 tree
= base_node_tree
1510 link_end
= output_socket
1511 while tree
.nodes
.active
!= active
:
1512 node
= tree
.nodes
.active
1513 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1514 link_start
= node
.outputs
[index
]
1515 node_socket
= node
.node_tree
.outputs
[index
]
1516 if node_socket
in delete_sockets
:
1517 delete_sockets
.remove(node_socket
)
1518 tree
.links
.new(link_start
, link_end
)
1520 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1521 tree
= tree
.nodes
.active
.node_tree
1522 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1525 for socket
in delete_sockets
:
1526 tree
= socket
.id_data
1527 tree
.outputs
.remove(socket
)
1529 nodes
.active
= active
1530 active
.select
= True
1531 force_update(context
)
1535 # What follows is code for the shader editor
1536 output_types
= [x
.nodetype
for x
in
1537 get_nodes_from_category('Output', context
)]
1540 if active
.rna_type
.identifier
not in output_types
:
1541 for out
in active
.outputs
:
1542 if is_visible_socket(out
):
1546 # get material_output node
1547 materialout
= None # placeholder node
1550 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1551 self
.scan_nodes(base_node_tree
, delete_sockets
)
1553 materialout
= self
.get_shader_output_node(base_node_tree
)
1555 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1556 materialout
.location
= get_output_location(base_node_tree
)
1557 materialout
.select
= False
1561 for i
, out
in enumerate(active
.outputs
):
1562 if is_visible_socket(out
):
1563 valid_outputs
.append(i
)
1565 out_i
= valid_outputs
[0] # Start index of node's outputs
1566 for i
, valid_i
in enumerate(valid_outputs
):
1567 for out_link
in active
.outputs
[valid_i
].links
:
1568 if is_viewer_link(out_link
, materialout
):
1569 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1570 if i
< len(valid_outputs
) - 1:
1571 out_i
= valid_outputs
[i
+ 1]
1573 out_i
= valid_outputs
[0]
1575 make_links
= [] # store sockets for new links
1577 socket_type
= 'NodeSocketShader'
1578 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
1579 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
1580 output_socket
= materialout
.inputs
[materialout_index
]
1581 for li_from
, li_to
in make_links
:
1582 base_node_tree
.links
.new(li_from
, li_to
)
1584 # Create links through node groups until we reach the active node
1585 tree
= base_node_tree
1586 link_end
= output_socket
1587 while tree
.nodes
.active
!= active
:
1588 node
= tree
.nodes
.active
1589 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1590 link_start
= node
.outputs
[index
]
1591 node_socket
= node
.node_tree
.outputs
[index
]
1592 if node_socket
in delete_sockets
:
1593 delete_sockets
.remove(node_socket
)
1594 tree
.links
.new(link_start
, link_end
)
1596 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1597 tree
= tree
.nodes
.active
.node_tree
1598 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1601 for socket
in delete_sockets
:
1602 if not self
.is_socket_used_other_mats(socket
):
1603 tree
= socket
.id_data
1604 tree
.outputs
.remove(socket
)
1606 nodes
.active
= active
1607 active
.select
= True
1609 force_update(context
)
1613 return {'CANCELLED'}
1616 class NWFrameSelected(Operator
, NWBase
):
1617 bl_idname
= "node.nw_frame_selected"
1618 bl_label
= "Frame Selected"
1619 bl_description
= "Add a frame node and parent the selected nodes to it"
1620 bl_options
= {'REGISTER', 'UNDO'}
1622 label_prop
: StringProperty(
1624 description
='The visual name of the frame node',
1627 use_custom_color_prop
: BoolProperty(
1628 name
="Custom Color",
1629 description
="Use custom color for the frame node",
1632 color_prop
: FloatVectorProperty(
1634 description
="The color of the frame node",
1635 default
=(0.604, 0.604, 0.604),
1636 min=0, max=1, step
=1, precision
=3,
1637 subtype
='COLOR_GAMMA', size
=3
1640 def draw(self
, context
):
1641 layout
= self
.layout
1642 layout
.prop(self
, 'label_prop')
1643 layout
.prop(self
, 'use_custom_color_prop')
1644 col
= layout
.column()
1645 col
.active
= self
.use_custom_color_prop
1646 col
.prop(self
, 'color_prop', text
="")
1648 def execute(self
, context
):
1649 nodes
, links
= get_nodes_links(context
)
1652 if node
.select
== True:
1653 selected
.append(node
)
1655 bpy
.ops
.node
.add_node(type='NodeFrame')
1657 frm
.label
= self
.label_prop
1658 frm
.use_custom_color
= self
.use_custom_color_prop
1659 frm
.color
= self
.color_prop
1661 for node
in selected
:
1667 class NWReloadImages(Operator
):
1668 bl_idname
= "node.nw_reload_images"
1669 bl_label
= "Reload Images"
1670 bl_description
= "Update all the image nodes to match their files on disk"
1673 def poll(cls
, context
):
1675 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
1676 if context
.active_node
is not None:
1677 for out
in context
.active_node
.outputs
:
1678 if is_visible_socket(out
):
1683 def execute(self
, context
):
1684 nodes
, links
= get_nodes_links(context
)
1685 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1688 if node
.type in image_types
:
1689 if node
.type == "TEXTURE":
1690 if node
.texture
: # node has texture assigned
1691 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1692 if node
.texture
.image
: # texture has image assigned
1693 node
.texture
.image
.reload()
1701 self
.report({'INFO'}, "Reloaded images")
1702 print("Reloaded " + str(num_reloaded
) + " images")
1703 force_update(context
)
1706 self
.report({'WARNING'}, "No images found to reload in this node tree")
1707 return {'CANCELLED'}
1710 class NWSwitchNodeType(Operator
, NWBase
):
1711 """Switch type of selected nodes """
1712 bl_idname
= "node.nw_swtch_node_type"
1713 bl_label
= "Switch Node Type"
1714 bl_options
= {'REGISTER', 'UNDO'}
1716 to_type
: StringProperty(
1717 name
="Switch to type",
1721 def execute(self
, context
):
1722 to_type
= self
.to_type
1723 if len(to_type
) == 0:
1724 return {'CANCELLED'}
1726 nodes
, links
= get_nodes_links(context
)
1727 # Those types of nodes will not swap.
1728 src_excludes
= ('NodeFrame')
1729 # Those attributes of nodes will be copied if possible
1730 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
1731 'show_options', 'show_preview', 'show_texture',
1732 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1734 selected
= [n
for n
in nodes
if n
.select
]
1736 for node
in [n
for n
in selected
if
1737 n
.rna_type
.identifier
not in src_excludes
and
1738 n
.rna_type
.identifier
!= to_type
]:
1739 new_node
= nodes
.new(to_type
)
1740 for attr
in attrs_to_pass
:
1741 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
1742 setattr(new_node
, attr
, getattr(node
, attr
))
1743 # set image datablock of dst to image of src
1744 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
1746 new_node
.image
= node
.image
1748 if new_node
.type == 'SWITCH':
1749 new_node
.hide
= True
1750 # Dictionaries: src_sockets and dst_sockets:
1751 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1752 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1753 # in 'INPUTS' and 'OUTPUTS':
1754 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1756 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1758 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1759 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1762 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1763 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1765 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1766 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1767 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1768 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
1769 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1770 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
1771 # enumerate in inputs, then in outputs
1772 # find name, default value and links of socket
1773 for i
, socket
in enumerate(in_out
):
1774 the_name
= socket
.name
1776 # Not every socket, especially in outputs has "default_value"
1777 if hasattr(socket
, 'default_value'):
1778 dval
= socket
.default_value
1780 for lnk
in socket
.links
:
1781 socket_links
.append(lnk
)
1782 # check type of socket to fill proper keys.
1783 for the_type
in types_order_one
:
1784 if socket
.type == the_type
:
1785 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1786 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1787 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
1788 # Check which of the types in inputs/outputs is considered to be "main".
1789 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1790 for type_check
in types_order_one
:
1791 if sockets
[in_out_name
][type_check
]:
1792 sockets
[in_out_name
]['MAIN'] = type_check
1796 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1797 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1800 for inout
, soctype
in (
1801 ('INPUTS', 'MAIN',),
1802 ('INPUTS', 'SHADER',),
1803 ('INPUTS', 'RGBA',),
1804 ('INPUTS', 'VECTOR',),
1805 ('INPUTS', 'VALUE',),
1806 ('OUTPUTS', 'MAIN',),
1807 ('OUTPUTS', 'SHADER',),
1808 ('OUTPUTS', 'RGBA',),
1809 ('OUTPUTS', 'VECTOR',),
1810 ('OUTPUTS', 'VALUE',),
1812 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
1813 if soctype
== 'MAIN':
1814 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
1815 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
1817 sc
= src_sockets
[inout
][soctype
]
1818 dt
= dst_sockets
[inout
][soctype
]
1819 # start with 'dt' to determine number of possibilities.
1820 for i
, soc
in enumerate(dt
):
1821 # if src main has enough entries - match them with dst main sockets by indexes.
1823 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
1824 # add 'VALUE_NAME' criterion to inputs.
1825 if inout
== 'INPUTS' and soctype
== 'VALUE':
1827 if s
[2] == soc
[2]: # if names match
1828 # append src (index, dval), dst (index, dval)
1829 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
1831 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1832 # This creates better links when relinking textures.
1833 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
1834 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
1836 # Pass default values and RELINK:
1837 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1838 # INPUTS: Base on matches in proper order.
1839 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
1841 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
1842 new_node
.inputs
[dst_i
].default_value
= src_dval
1843 # Special case: switch to math
1844 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1845 new_node
.type == 'MATH' and\
1847 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
1848 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
1849 if node
.type == 'MIX_RGB':
1850 if node
.blend_type
in [o
[0] for o
in operations
]:
1851 new_node
.operation
= node
.blend_type
1852 # Special case: switch from math to some types
1853 if node
.type == 'MATH' and\
1854 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1857 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
1858 if new_node
.type == 'MIX_RGB':
1859 if node
.operation
in [t
[0] for t
in blend_types
]:
1860 new_node
.blend_type
= node
.operation
1861 # Set Fac of MIX_RGB to 1.0
1862 new_node
.inputs
[0].default_value
= 1.0
1863 # make link only when dst matching input is not linked already.
1864 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
1865 in_src_link
= node
.inputs
[src_i
].links
[0]
1866 in_dst_socket
= new_node
.inputs
[dst_i
]
1867 links
.new(in_src_link
.from_socket
, in_dst_socket
)
1868 links
.remove(in_src_link
)
1869 # OUTPUTS: Base on matches in proper order.
1870 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
1871 for out_src_link
in node
.outputs
[src_i
].links
:
1872 out_dst_socket
= new_node
.outputs
[dst_i
]
1873 links
.new(out_dst_socket
, out_src_link
.to_socket
)
1874 # relink rest inputs if possible, no criteria
1875 for src_inp
in node
.inputs
:
1876 for dst_inp
in new_node
.inputs
:
1877 if src_inp
.links
and not dst_inp
.links
:
1878 src_link
= src_inp
.links
[0]
1879 links
.new(src_link
.from_socket
, dst_inp
)
1880 links
.remove(src_link
)
1881 # relink rest outputs if possible, base on node kind if any left.
1882 for src_o
in node
.outputs
:
1883 for out_src_link
in src_o
.links
:
1884 for dst_o
in new_node
.outputs
:
1885 if src_o
.type == dst_o
.type:
1886 links
.new(dst_o
, out_src_link
.to_socket
)
1887 # relink rest outputs no criteria if any left. Link all from first output.
1888 for src_o
in node
.outputs
:
1889 for out_src_link
in src_o
.links
:
1890 if new_node
.outputs
:
1891 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
1893 force_update(context
)
1897 class NWMergeNodes(Operator
, NWBase
):
1898 bl_idname
= "node.nw_merge_nodes"
1899 bl_label
= "Merge Nodes"
1900 bl_description
= "Merge Selected Nodes"
1901 bl_options
= {'REGISTER', 'UNDO'}
1905 description
="All possible blend types, boolean operations and math operations",
1906 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
],
1908 merge_type
: EnumProperty(
1910 description
="Type of Merge to be used",
1912 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1913 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1914 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
1915 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1916 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1917 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
1918 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
1922 # Check if the link connects to a node that is in selected_nodes
1923 # If not, then check recursively for each link in the nodes outputs.
1924 # If yes, return True. If the recursion stops without finding a node
1925 # in selected_nodes, it returns False. The depth is used to prevent
1926 # getting stuck in a loop because of an already present cycle.
1928 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
1930 # We're stuck in a cycle, but that cycle was already present,
1931 # so we return False.
1932 # NOTE: The number 255 is arbitrary, but seems to work well.
1935 if node
in selected_nodes
:
1937 if not node
.outputs
:
1939 for output
in node
.outputs
:
1940 if output
.is_linked
:
1941 for olink
in output
.links
:
1942 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
1944 # None of the outputs found a node in selected_nodes, so there is no cycle.
1947 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
1948 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
1949 # be connected. The last one is assumed to be a multi input socket.
1950 # For convenience the node is returned.
1952 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
1953 # The y-location of the last node
1954 loc_y
= nodes_list
[-1][2]
1955 if merge_position
== 'CENTER':
1956 # Average the y-location
1957 for i
in range(len(nodes_list
)-1):
1958 loc_y
+= nodes_list
[i
][2]
1959 loc_y
= loc_y
/len(nodes_list
)
1960 new_node
= nodes
.new(node_name
)
1961 new_node
.hide
= do_hide
1962 new_node
.location
.x
= loc_x
1963 new_node
.location
.y
= loc_y
1964 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
1966 outputs_for_multi_input
= []
1967 for i
,node
in enumerate(selected_nodes
):
1969 # Search for the first node which had output links that do not create
1970 # a cycle, which we can then reconnect afterwards.
1971 if prev_links
== [] and node
.outputs
[0].is_linked
:
1972 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
1973 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
1974 # To get the placement to look right we need to reverse the order in which we connect the
1975 # outputs to the multi input socket.
1976 if i
< len(socket_indices
) - 1:
1977 ind
= socket_indices
[i
]
1978 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
1980 outputs_for_multi_input
.insert(0, node
.outputs
[0])
1981 if outputs_for_multi_input
!= []:
1982 ind
= socket_indices
[-1]
1983 for output
in outputs_for_multi_input
:
1984 links
.new(output
, new_node
.inputs
[ind
])
1985 if prev_links
!= []:
1986 for link
in prev_links
:
1987 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
1990 def execute(self
, context
):
1991 settings
= context
.preferences
.addons
[__name__
].preferences
1992 merge_hide
= settings
.merge_hide
1993 merge_position
= settings
.merge_position
# 'center' or 'bottom'
1996 do_hide_shader
= False
1997 if merge_hide
== 'ALWAYS':
1999 do_hide_shader
= True
2000 elif merge_hide
== 'NON_SHADER':
2003 tree_type
= context
.space_data
.node_tree
.type
2004 if tree_type
== 'GEOMETRY':
2005 node_type
= 'GeometryNode'
2006 if tree_type
== 'COMPOSITING':
2007 node_type
= 'CompositorNode'
2008 elif tree_type
== 'SHADER':
2009 node_type
= 'ShaderNode'
2010 elif tree_type
== 'TEXTURE':
2011 node_type
= 'TextureNode'
2012 nodes
, links
= get_nodes_links(context
)
2014 merge_type
= self
.merge_type
2015 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2016 # 'ZCOMBINE' works only if mode == 'MIX'
2017 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2018 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2021 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2023 # The MixRGB node and math nodes used for geometry nodes are of type 'ShaderNode'
2024 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
2025 node_type
= 'ShaderNode'
2026 selected_mix
= [] # entry = [index, loc]
2027 selected_shader
= [] # entry = [index, loc]
2028 selected_geometry
= [] # entry = [index, loc]
2029 selected_math
= [] # entry = [index, loc]
2030 selected_vector
= [] # entry = [index, loc]
2031 selected_z
= [] # entry = [index, loc]
2032 selected_alphaover
= [] # entry = [index, loc]
2034 for i
, node
in enumerate(nodes
):
2035 if node
.select
and node
.outputs
:
2036 if merge_type
== 'AUTO':
2037 for (type, types_list
, dst
) in (
2038 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2039 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2040 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2041 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2042 ('VECTOR', [], selected_vector
),
2044 output
= get_first_enabled_output(node
)
2045 output_type
= output
.type
2046 valid_mode
= mode
in types_list
2047 # When mode is 'MIX' we have to cheat since the mix node is not used in
2049 if tree_type
== 'GEOMETRY':
2051 if output_type
== 'VALUE' and type == 'VALUE':
2053 elif output_type
== 'VECTOR' and type == 'VECTOR':
2055 elif type == 'GEOMETRY':
2057 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2058 # Cheat that output type is 'RGBA',
2059 # and that 'MIX' exists in math operations list.
2060 # This way when selected_mix list is analyzed:
2061 # Node data will be appended even though it doesn't meet requirements.
2062 elif output_type
!= 'SHADER' and mode
== 'MIX':
2063 output_type
= 'RGBA'
2065 if output_type
== type and valid_mode
:
2066 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2068 for (type, types_list
, dst
) in (
2069 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2070 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2071 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2072 ('MATH', [t
[0] for t
in operations
], selected_math
),
2073 ('ZCOMBINE', ('MIX', ), selected_z
),
2074 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2076 if merge_type
== type and mode
in types_list
:
2077 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2078 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2079 # use only 'Mix' nodes for merging.
2080 # For that we add selected_math list to selected_mix list and clear selected_math.
2081 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2082 selected_mix
+= selected_math
2084 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2087 count_before
= len(nodes
)
2088 # sort list by loc_x - reversed
2089 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2091 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2092 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2094 # Change the node type for math nodes in a geometry node tree.
2095 if tree_type
== 'GEOMETRY':
2096 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
2097 node_type
= 'ShaderNode'
2101 node_type
= 'GeometryNode'
2102 if merge_position
== 'CENTER':
2103 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)
2104 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2110 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2114 if nodes_list
== selected_shader
and not do_hide_shader
:
2116 the_range
= len(nodes_list
) - 1
2117 if len(nodes_list
) == 1:
2120 for i
in range(the_range
):
2121 if nodes_list
== selected_mix
:
2122 add_type
= node_type
+ 'MixRGB'
2123 add
= nodes
.new(add_type
)
2124 add
.blend_type
= mode
2126 add
.inputs
[0].default_value
= 1.0
2127 add
.show_preview
= False
2133 add
.width_hidden
= 100.0
2134 elif nodes_list
== selected_math
:
2135 add_type
= node_type
+ 'Math'
2136 add
= nodes
.new(add_type
)
2137 add
.operation
= mode
2143 add
.width_hidden
= 100.0
2144 elif nodes_list
== selected_shader
:
2146 add_type
= node_type
+ 'MixShader'
2147 add
= nodes
.new(add_type
)
2148 add
.hide
= do_hide_shader
2153 add
.width_hidden
= 100.0
2155 add_type
= node_type
+ 'AddShader'
2156 add
= nodes
.new(add_type
)
2157 add
.hide
= do_hide_shader
2162 add
.width_hidden
= 100.0
2163 elif nodes_list
== selected_geometry
:
2164 if mode
in ('JOIN', 'MIX'):
2165 add_type
= node_type
+ 'JoinGeometry'
2166 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2168 add_type
= node_type
+ 'Boolean'
2169 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2170 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2171 add
.operation
= mode
2174 elif nodes_list
== selected_vector
:
2175 add_type
= node_type
+ 'VectorMath'
2176 add
= nodes
.new(add_type
)
2177 add
.operation
= mode
2183 add
.width_hidden
= 100.0
2184 elif nodes_list
== selected_z
:
2185 add
= nodes
.new('CompositorNodeZcombine')
2186 add
.show_preview
= False
2192 add
.width_hidden
= 100.0
2193 elif nodes_list
== selected_alphaover
:
2194 add
= nodes
.new('CompositorNodeAlphaOver')
2195 add
.show_preview
= False
2201 add
.width_hidden
= 100.0
2202 add
.location
= loc_x
, loc_y
2206 # This has already been handled separately
2210 count_after
= len(nodes
)
2211 index
= count_after
- 1
2212 first_selected
= nodes
[nodes_list
[0][0]]
2213 # "last" node has been added as first, so its index is count_before.
2214 last_add
= nodes
[count_before
]
2215 # Create list of invalid indexes.
2216 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2219 # Two nodes were selected and first selected has no output links, second selected has output links.
2220 # Then add links from last add to all links 'to_socket' of out links of second selected.
2221 first_selected_output
= get_first_enabled_output(first_selected
)
2222 if len(nodes_list
) == 2:
2223 if not first_selected_output
.links
:
2224 second_selected
= nodes
[nodes_list
[1][0]]
2225 for ss_link
in second_selected
.outputs
[0].links
:
2226 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2227 # Link only if "to_node" index not in invalid indexes list.
2228 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2229 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2230 # add links from last_add to all links 'to_socket' of out links of first selected.
2231 for fs_link
in first_selected_output
.links
:
2232 # Link only if "to_node" index not in invalid indexes list.
2233 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2234 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2235 # add link from "first" selected and "first" add node
2236 node_to
= nodes
[count_after
- 1]
2237 links
.new(first_selected_output
, node_to
.inputs
[first
])
2238 if node_to
.type == 'ZCOMBINE':
2239 for fs_out
in first_selected
.outputs
:
2240 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
2241 links
.new(fs_out
, node_to
.inputs
[1])
2243 # add links between added ADD nodes and between selected and ADD nodes
2244 for i
in range(count_adds
):
2245 if i
< count_adds
- 1:
2246 node_from
= nodes
[index
]
2247 node_to
= nodes
[index
- 1]
2248 node_to_input_i
= first
2249 node_to_z_i
= 1 # if z combine - link z to first z input
2250 links
.new(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
2251 if node_to
.type == 'ZCOMBINE':
2252 for from_out
in node_from
.outputs
:
2253 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
2254 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2255 if len(nodes_list
) > 1:
2256 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2257 node_to
= nodes
[index
]
2258 node_to_input_i
= second
2259 node_to_z_i
= 3 # if z combine - link z to second z input
2260 links
.new(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
2261 if node_to
.type == 'ZCOMBINE':
2262 for from_out
in node_from
.outputs
:
2263 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
2264 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2266 # set "last" of added nodes as active
2267 nodes
.active
= last_add
2268 for i
, x
, y
, dx
, h
in nodes_list
:
2269 nodes
[i
].select
= False
2274 class NWBatchChangeNodes(Operator
, NWBase
):
2275 bl_idname
= "node.nw_batch_change"
2276 bl_label
= "Batch Change"
2277 bl_description
= "Batch Change Blend Type and Math Operation"
2278 bl_options
= {'REGISTER', 'UNDO'}
2280 blend_type
: EnumProperty(
2282 items
=blend_types
+ navs
,
2284 operation
: EnumProperty(
2286 items
=operations
+ navs
,
2289 def execute(self
, context
):
2290 blend_type
= self
.blend_type
2291 operation
= self
.operation
2292 for node
in context
.selected_nodes
:
2293 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2294 if not blend_type
in [nav
[0] for nav
in navs
]:
2295 node
.blend_type
= blend_type
2297 if blend_type
== 'NEXT':
2298 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2299 #index = blend_types.index(node.blend_type)
2300 if index
== len(blend_types
) - 1:
2301 node
.blend_type
= blend_types
[0][0]
2303 node
.blend_type
= blend_types
[index
+ 1][0]
2305 if blend_type
== 'PREV':
2306 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2308 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2310 node
.blend_type
= blend_types
[index
- 1][0]
2312 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2313 if not operation
in [nav
[0] for nav
in navs
]:
2314 node
.operation
= operation
2316 if operation
== 'NEXT':
2317 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2318 #index = operations.index(node.operation)
2319 if index
== len(operations
) - 1:
2320 node
.operation
= operations
[0][0]
2322 node
.operation
= operations
[index
+ 1][0]
2324 if operation
== 'PREV':
2325 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2326 #index = operations.index(node.operation)
2328 node
.operation
= operations
[len(operations
) - 1][0]
2330 node
.operation
= operations
[index
- 1][0]
2335 class NWChangeMixFactor(Operator
, NWBase
):
2336 bl_idname
= "node.nw_factor"
2337 bl_label
= "Change Factor"
2338 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2339 bl_options
= {'REGISTER', 'UNDO'}
2341 # option: Change factor.
2342 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2343 # Else - change factor by option value.
2344 option
: FloatProperty()
2346 def execute(self
, context
):
2347 nodes
, links
= get_nodes_links(context
)
2348 option
= self
.option
2349 selected
= [] # entry = index
2350 for si
, node
in enumerate(nodes
):
2352 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2356 fac
= nodes
[si
].inputs
[0]
2357 nodes
[si
].hide
= False
2358 if option
in {0.0, 1.0}:
2359 fac
.default_value
= option
2361 fac
.default_value
+= option
2366 class NWCopySettings(Operator
, NWBase
):
2367 bl_idname
= "node.nw_copy_settings"
2368 bl_label
= "Copy Settings"
2369 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2370 bl_options
= {'REGISTER', 'UNDO'}
2373 def poll(cls
, context
):
2375 if nw_check(context
):
2377 context
.active_node
is not None and
2378 context
.active_node
.type != 'FRAME'
2383 def execute(self
, context
):
2384 node_active
= context
.active_node
2385 node_selected
= context
.selected_nodes
2388 if not (len(node_selected
) > 1):
2389 self
.report({'ERROR'}, "2 nodes must be selected at least")
2390 return {'CANCELLED'}
2392 # Check if active node is in the selection
2393 selected_node_names
= [n
.name
for n
in node_selected
]
2394 if node_active
.name
not in selected_node_names
:
2395 self
.report({'ERROR'}, "No active node")
2396 return {'CANCELLED'}
2398 # Get nodes in selection by type
2399 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2401 if not (len(valid_nodes
) > 1) and node_active
:
2402 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2403 return {'CANCELLED'}
2405 if len(valid_nodes
) != len(node_selected
):
2406 # Report nodes that are not valid
2407 valid_node_names
= [n
.name
for n
in valid_nodes
]
2408 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2409 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2411 # Reference original
2413 #node_selected_names = [n.name for n in node_selected]
2418 # Deselect all nodes
2419 for i
in node_selected
:
2422 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2423 # Run through all other nodes
2424 for node
in valid_nodes
[1:]:
2426 # Check for frame node
2427 parent
= node
.parent
if node
.parent
else None
2428 node_loc
= [node
.location
.x
, node
.location
.y
]
2430 # Select original to duplicate
2433 # Duplicate selected node
2434 bpy
.ops
.node
.duplicate()
2435 new_node
= context
.selected_nodes
[0]
2438 new_node
.select
= False
2440 # Properties to copy
2441 node_tree
= node
.id_data
2442 props_to_copy
= 'bl_idname name location height width'.split(' ')
2446 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2447 for i
in (i
for i
in mappings
if i
.is_linked
):
2449 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2452 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2453 props_to_copy
.pop(0)
2455 for prop
in props_to_copy
:
2456 setattr(new_node
, prop
, props
[prop
])
2458 # Get the node tree to remove the old node
2459 nodes
= node_tree
.nodes
2461 new_node
.name
= props
['name']
2464 new_node
.parent
= parent
2465 new_node
.location
= node_loc
2467 for str_from
, str_to
in reconnections
:
2468 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2470 success_names
.append(new_node
.name
)
2473 node_tree
.nodes
.active
= orig
2474 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2478 class NWCopyLabel(Operator
, NWBase
):
2479 bl_idname
= "node.nw_copy_label"
2480 bl_label
= "Copy Label"
2481 bl_options
= {'REGISTER', 'UNDO'}
2483 option
: EnumProperty(
2485 description
="Source of name of label",
2487 ('FROM_ACTIVE', 'from active', 'from active node',),
2488 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2489 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2493 def execute(self
, context
):
2494 nodes
, links
= get_nodes_links(context
)
2495 option
= self
.option
2496 active
= nodes
.active
2497 if option
== 'FROM_ACTIVE':
2499 src_label
= active
.label
2500 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2501 node
.label
= src_label
2502 elif option
== 'FROM_NODE':
2503 selected
= [n
for n
in nodes
if n
.select
]
2504 for node
in selected
:
2505 for input in node
.inputs
:
2507 src
= input.links
[0].from_node
2508 node
.label
= src
.label
2510 elif option
== 'FROM_SOCKET':
2511 selected
= [n
for n
in nodes
if n
.select
]
2512 for node
in selected
:
2513 for input in node
.inputs
:
2515 src
= input.links
[0].from_socket
2516 node
.label
= src
.name
2522 class NWClearLabel(Operator
, NWBase
):
2523 bl_idname
= "node.nw_clear_label"
2524 bl_label
= "Clear Label"
2525 bl_options
= {'REGISTER', 'UNDO'}
2527 option
: BoolProperty()
2529 def execute(self
, context
):
2530 nodes
, links
= get_nodes_links(context
)
2531 for node
in [n
for n
in nodes
if n
.select
]:
2536 def invoke(self
, context
, event
):
2538 return self
.execute(context
)
2540 return context
.window_manager
.invoke_confirm(self
, event
)
2543 class NWModifyLabels(Operator
, NWBase
):
2544 """Modify Labels of all selected nodes"""
2545 bl_idname
= "node.nw_modify_labels"
2546 bl_label
= "Modify Labels"
2547 bl_options
= {'REGISTER', 'UNDO'}
2549 prepend
: StringProperty(
2550 name
="Add to Beginning"
2552 append
: StringProperty(
2555 replace_from
: StringProperty(
2556 name
="Text to Replace"
2558 replace_to
: StringProperty(
2562 def execute(self
, context
):
2563 nodes
, links
= get_nodes_links(context
)
2564 for node
in [n
for n
in nodes
if n
.select
]:
2565 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
2569 def invoke(self
, context
, event
):
2573 return context
.window_manager
.invoke_props_dialog(self
)
2576 class NWAddTextureSetup(Operator
, NWBase
):
2577 bl_idname
= "node.nw_add_texture"
2578 bl_label
= "Texture Setup"
2579 bl_description
= "Add Texture Node Setup to Selected Shaders"
2580 bl_options
= {'REGISTER', 'UNDO'}
2582 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
2585 def poll(cls
, context
):
2586 if nw_check(context
):
2587 space
= context
.space_data
2588 if space
.tree_type
== 'ShaderNodeTree':
2592 def execute(self
, context
):
2593 nodes
, links
= get_nodes_links(context
)
2595 texture_types
= [x
.nodetype
for x
in
2596 get_nodes_from_category('Texture', context
)]
2597 selected_nodes
= [n
for n
in nodes
if n
.select
]
2599 for node
in selected_nodes
:
2604 target_input
= node
.inputs
[0]
2605 for input in node
.inputs
:
2608 if not input.is_linked
:
2609 target_input
= input
2612 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
2617 locx
= node
.location
.x
2618 locy
= node
.location
.y
- (input_index
* padding
)
2620 is_texture_node
= node
.rna_type
.identifier
in texture_types
2621 use_environment_texture
= node
.type == 'BACKGROUND'
2623 # Add an image texture before normal shader nodes.
2624 if not is_texture_node
:
2625 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
2626 image_texture_node
= nodes
.new(image_texture_type
)
2627 x_offset
= x_offset
+ image_texture_node
.width
+ padding
2628 image_texture_node
.location
= [locx
- x_offset
, locy
]
2629 nodes
.active
= image_texture_node
2630 links
.new(image_texture_node
.outputs
[0], target_input
)
2632 # The mapping setup following this will connect to the firrst input of this image texture.
2633 target_input
= image_texture_node
.inputs
[0]
2637 if is_texture_node
or self
.add_mapping
:
2639 mapping_node
= nodes
.new('ShaderNodeMapping')
2640 x_offset
= x_offset
+ mapping_node
.width
+ padding
2641 mapping_node
.location
= [locx
- x_offset
, locy
]
2642 links
.new(mapping_node
.outputs
[0], target_input
)
2644 # Add Texture Coordinates node.
2645 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
2646 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
2647 tex_coord_node
.location
= [locx
- x_offset
, locy
]
2649 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
2650 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
2651 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
2652 links
.new(tex_coord_output
, mapping_node
.inputs
[0])
2657 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
2658 bl_idname
= "node.nw_add_textures_for_principled"
2659 bl_label
= "Principled Texture Setup"
2660 bl_description
= "Add Texture Node Setup for Principled BSDF"
2661 bl_options
= {'REGISTER', 'UNDO'}
2663 directory
: StringProperty(
2667 description
='Folder to search in for image files'
2669 files
: CollectionProperty(
2670 type=bpy
.types
.OperatorFileListElement
,
2671 options
={'HIDDEN', 'SKIP_SAVE'}
2674 relative_path
: BoolProperty(
2675 name
='Relative Path',
2676 description
='Set the file path relative to the blend file, when possible',
2685 def draw(self
, context
):
2686 layout
= self
.layout
2687 layout
.alignment
= 'LEFT'
2689 layout
.prop(self
, 'relative_path')
2692 def poll(cls
, context
):
2694 if nw_check(context
):
2695 space
= context
.space_data
2696 if space
.tree_type
== 'ShaderNodeTree':
2700 def execute(self
, context
):
2701 # Check if everything is ok
2702 if not self
.directory
:
2703 self
.report({'INFO'}, 'No Folder Selected')
2704 return {'CANCELLED'}
2705 if not self
.files
[:]:
2706 self
.report({'INFO'}, 'No Files Selected')
2707 return {'CANCELLED'}
2709 nodes
, links
= get_nodes_links(context
)
2710 active_node
= nodes
.active
2711 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
2712 self
.report({'INFO'}, 'Select Principled BSDF')
2713 return {'CANCELLED'}
2716 def split_into__components(fname
):
2717 # Split filename into components
2718 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2720 fname
= path
.splitext(fname
)[0]
2722 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
2723 # Separate CamelCase by space
2724 fname
= re
.sub(r
"([a-z])([A-Z])", r
"\g<1> \g<2>",fname
)
2725 # Replace common separators with SPACE
2726 separators
= ['_', '.', '-', '__', '--', '#']
2727 for sep
in separators
:
2728 fname
= fname
.replace(sep
, ' ')
2730 components
= fname
.split(' ')
2731 components
= [c
.lower() for c
in components
]
2734 # Filter textures names for texturetypes in filenames
2735 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2736 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
2737 normal_abbr
= tags
.normal
.split(' ')
2738 bump_abbr
= tags
.bump
.split(' ')
2739 gloss_abbr
= tags
.gloss
.split(' ')
2740 rough_abbr
= tags
.rough
.split(' ')
2742 ['Displacement', tags
.displacement
.split(' '), None],
2743 ['Base Color', tags
.base_color
.split(' '), None],
2744 ['Subsurface Color', tags
.sss_color
.split(' '), None],
2745 ['Metallic', tags
.metallic
.split(' '), None],
2746 ['Specular', tags
.specular
.split(' '), None],
2747 ['Roughness', rough_abbr
+ gloss_abbr
, None],
2748 ['Normal', normal_abbr
+ bump_abbr
, None],
2749 ['Transmission', tags
.transmission
.split(' '), None],
2750 ['Emission', tags
.emission
.split(' '), None],
2751 ['Alpha', tags
.alpha
.split(' '), None],
2752 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
2755 # Look through texture_types and set value as filename of first matched file
2756 def match_files_to_socket_names():
2757 for sname
in socketnames
:
2758 for file in self
.files
:
2760 filenamecomponents
= split_into__components(fname
)
2761 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
2762 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2767 match_files_to_socket_names()
2768 # Remove socketnames without found files
2769 socketnames
= [s
for s
in socketnames
if s
[2]
2770 and path
.exists(self
.directory
+s
[2])]
2772 self
.report({'INFO'}, 'No matching images found')
2773 print('No matching images found')
2774 return {'CANCELLED'}
2776 # Don't override path earlier as os.path is used to check the absolute path
2777 import_path
= self
.directory
2778 if self
.relative_path
:
2779 if bpy
.data
.filepath
:
2781 import_path
= bpy
.path
.relpath(self
.directory
)
2786 print('\nMatched Textures:')
2791 roughness_node
= None
2792 for i
, sname
in enumerate(socketnames
):
2793 print(i
, sname
[0], sname
[2])
2795 # DISPLACEMENT NODES
2796 if sname
[0] == 'Displacement':
2797 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
2798 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2799 disp_texture
.image
= img
2800 disp_texture
.label
= 'Displacement'
2801 if disp_texture
.image
:
2802 disp_texture
.image
.colorspace_settings
.is_data
= True
2804 # Add displacement offset nodes
2805 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
2806 # Align the Displacement node under the active Principled BSDF node
2807 disp_node
.location
= active_node
.location
+ Vector((100, -700))
2808 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
2810 # TODO Turn on true displacement in the material
2811 # Too complicated for now
2814 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
2816 if not output_node
[0].inputs
[2].is_linked
:
2817 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
2821 # AMBIENT OCCLUSION TEXTURE
2822 if sname
[0] == 'Ambient Occlusion':
2823 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
2824 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2825 ao_texture
.image
= img
2826 ao_texture
.label
= sname
[0]
2827 if ao_texture
.image
:
2828 ao_texture
.image
.colorspace_settings
.is_data
= True
2832 if not active_node
.inputs
[sname
[0]].is_linked
:
2833 # No texture node connected -> add texture node with new image
2834 texture_node
= nodes
.new(type='ShaderNodeTexImage')
2835 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2836 texture_node
.image
= img
2839 if sname
[0] == 'Normal':
2840 # Test if new texture node is normal or bump map
2841 fname_components
= split_into__components(sname
[2])
2842 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
2843 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
2845 # If Normal add normal node in between
2846 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
2847 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
2849 # If Bump add bump node in between
2850 normal_node
= nodes
.new(type='ShaderNodeBump')
2851 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
2853 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
2854 normal_node_texture
= texture_node
2856 elif sname
[0] == 'Roughness':
2857 # Test if glossy or roughness map
2858 fname_components
= split_into__components(sname
[2])
2859 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
2860 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
2863 # If Roughness nothing to to
2864 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2867 # If Gloss Map add invert node
2868 invert_node
= nodes
.new(type='ShaderNodeInvert')
2869 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
2871 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
2872 roughness_node
= texture_node
2875 # This is a simple connection Texture --> Input slot
2876 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2878 # Use non-color for all but 'Base Color' Textures
2879 if not sname
[0] in ['Base Color', 'Emission'] and texture_node
.image
:
2880 texture_node
.image
.colorspace_settings
.is_data
= True
2883 # If already texture connected. add to node list for alignment
2884 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
2886 # This are all connected texture nodes
2887 texture_nodes
.append(texture_node
)
2888 texture_node
.label
= sname
[0]
2891 texture_nodes
.append(disp_texture
)
2894 # We want the ambient occlusion texture to be the top most texture node
2895 texture_nodes
.insert(0, ao_texture
)
2898 for i
, texture_node
in enumerate(texture_nodes
):
2899 offset
= Vector((-550, (i
* -280) + 200))
2900 texture_node
.location
= active_node
.location
+ offset
2903 # Extra alignment if normal node was added
2904 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
2907 # Alignment of invert node if glossy map
2908 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
2910 # Add texture input + mapping
2911 mapping
= nodes
.new(type='ShaderNodeMapping')
2912 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
2913 if len(texture_nodes
) > 1:
2914 # If more than one texture add reroute node in between
2915 reroute
= nodes
.new(type='NodeReroute')
2916 texture_nodes
.append(reroute
)
2917 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
2918 reroute
.location
= tex_coords
+ Vector((-50, -120))
2919 for texture_node
in texture_nodes
:
2920 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
2921 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
2923 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
2925 # Connect texture_coordiantes to mapping node
2926 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
2927 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
2928 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
2930 # Create frame around tex coords and mapping
2931 frame
= nodes
.new(type='NodeFrame')
2932 frame
.label
= 'Mapping'
2933 mapping
.parent
= frame
2934 texture_input
.parent
= frame
2937 # Create frame around texture nodes
2938 frame
= nodes
.new(type='NodeFrame')
2939 frame
.label
= 'Textures'
2940 for tnode
in texture_nodes
:
2941 tnode
.parent
= frame
2945 active_node
.select
= False
2948 force_update(context
)
2952 class NWAddReroutes(Operator
, NWBase
):
2953 """Add Reroute Nodes and link them to outputs of selected nodes"""
2954 bl_idname
= "node.nw_add_reroutes"
2955 bl_label
= "Add Reroutes"
2956 bl_description
= "Add Reroutes to Outputs"
2957 bl_options
= {'REGISTER', 'UNDO'}
2959 option
: EnumProperty(
2962 ('ALL', 'to all', 'Add to all outputs'),
2963 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2964 ('LINKED', 'to linked', 'Add only to linked outputs'),
2968 def execute(self
, context
):
2969 tree_type
= context
.space_data
.node_tree
.type
2970 option
= self
.option
2971 nodes
, links
= get_nodes_links(context
)
2972 # output valid when option is 'all' or when 'loose' output has no links
2974 post_select
= [] # nodes to be selected after execution
2975 # create reroutes and recreate links
2976 for node
in [n
for n
in nodes
if n
.select
]:
2981 # unhide 'REROUTE' nodes to avoid issues with location.y
2982 if node
.type == 'REROUTE':
2984 # When node is hidden - width_hidden not usable.
2985 # Hack needed to calculate real width
2987 bpy
.ops
.node
.select_all(action
='DESELECT')
2988 helper
= nodes
.new('NodeReroute')
2989 helper
.select
= True
2991 # resize node and helper to zero. Then check locations to calculate width
2992 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2993 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2994 # restore node location
2995 node
.location
= x
, y
2998 # only helper is selected now
2999 bpy
.ops
.node
.delete()
3000 x
= node
.location
.x
+ width
+ 20.0
3001 if node
.type != 'REROUTE':
3005 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3006 for out_i
, output
in enumerate(node
.outputs
):
3007 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3008 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3009 if node
.type != 'R_LAYERS':
3011 else: # if 'R_LAYERS' check if output represent used render pass
3012 node_scene
= node
.scene
3013 node_layer
= node
.layer
3014 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3015 if output
.name
== 'Alpha':
3018 # check entries in global 'rl_outputs' variable
3019 for rlo
in rl_outputs
:
3020 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3021 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3024 valid
= ((option
== 'ALL') or
3025 (option
== 'LOOSE' and not output
.links
) or
3026 (option
== 'LINKED' and output
.links
))
3027 # Add reroutes only if valid, but offset location in all cases.
3029 n
= nodes
.new('NodeReroute')
3031 for link
in output
.links
:
3032 links
.new(n
.outputs
[0], link
.to_socket
)
3033 links
.new(output
, n
.inputs
[0])
3035 post_select
.append(n
)
3039 # disselect the node so that after execution of script only newly created nodes are selected
3041 # nicer reroutes distribution along y when node.hide
3043 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3044 for reroute
in [r
for r
in nodes
if r
.select
]:
3045 reroute
.location
.y
-= y_translate
3046 for node
in post_select
:
3052 class NWLinkActiveToSelected(Operator
, NWBase
):
3053 """Link active node to selected nodes basing on various criteria"""
3054 bl_idname
= "node.nw_link_active_to_selected"
3055 bl_label
= "Link Active Node to Selected"
3056 bl_options
= {'REGISTER', 'UNDO'}
3058 replace
: BoolProperty()
3059 use_node_name
: BoolProperty()
3060 use_outputs_names
: BoolProperty()
3063 def poll(cls
, context
):
3065 if nw_check(context
):
3066 if context
.active_node
is not None:
3067 if context
.active_node
.select
:
3071 def execute(self
, context
):
3072 nodes
, links
= get_nodes_links(context
)
3073 replace
= self
.replace
3074 use_node_name
= self
.use_node_name
3075 use_outputs_names
= self
.use_outputs_names
3076 active
= nodes
.active
3077 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3078 outputs
= [] # Only usable outputs of active nodes will be stored here.
3079 for out
in active
.outputs
:
3080 if active
.type != 'R_LAYERS':
3083 # 'R_LAYERS' node type needs special handling.
3084 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3085 # Only outputs that represent used passes should be taken into account
3086 # Check if pass represented by output is used.
3087 # global 'rl_outputs' list will be used for that
3088 for rlo
in rl_outputs
:
3089 pass_used
= False # initial value. Will be set to True if pass is used
3090 if out
.name
== 'Alpha':
3091 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3093 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3094 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3095 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3099 doit
= True # Will be changed to False when links successfully added to previous output.
3102 for node
in selected
:
3103 dst_name
= node
.name
# Will be compared with src_name if needed.
3104 # When node has label - use it as dst_name
3106 dst_name
= node
.label
3107 valid
= True # Initial value. Will be changed to False if names don't match.
3108 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
3110 # Set src_name to source node name or label
3111 src_name
= active
.name
3113 src_name
= active
.label
3114 elif use_outputs_names
:
3115 src_name
= (out
.name
, )
3116 for rlo
in rl_outputs
:
3117 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3118 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3119 if dst_name
not in src_name
:
3122 for input in node
.inputs
:
3123 if input.type == out
.type or node
.type == 'REROUTE':
3124 if replace
or not input.is_linked
:
3125 links
.new(out
, input)
3126 if not use_node_name
and not use_outputs_names
:
3133 class NWAlignNodes(Operator
, NWBase
):
3134 '''Align the selected nodes neatly in a row/column'''
3135 bl_idname
= "node.nw_align_nodes"
3136 bl_label
= "Align Nodes"
3137 bl_options
= {'REGISTER', 'UNDO'}
3138 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3140 def execute(self
, context
):
3141 nodes
, links
= get_nodes_links(context
)
3142 margin
= self
.margin
3146 if node
.select
and node
.type != 'FRAME':
3147 selection
.append(node
)
3149 # If no nodes are selected, align all nodes
3153 elif nodes
.active
in selection
:
3154 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3156 # Check if nodes should be laid out horizontally or vertically
3157 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3158 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3159 x_range
= max(x_locs
) - min(x_locs
)
3160 y_range
= max(y_locs
) - min(y_locs
)
3161 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3162 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3163 horizontal
= x_range
> y_range
3165 # Sort selection by location of node mid-point
3167 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3169 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3173 for node
in selection
:
3174 current_margin
= margin
3175 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3178 node
.location
.x
= current_pos
3179 current_pos
+= current_margin
+ node
.dimensions
.x
3180 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3182 node
.location
.y
= current_pos
3183 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3184 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3186 # If active node is selected, center nodes around it
3187 if active_loc
is not None:
3188 active_loc_diff
= active_loc
- nodes
.active
.location
3189 for node
in selection
:
3190 node
.location
+= active_loc_diff
3191 else: # Position nodes centered around where they used to be
3192 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
])
3193 new_mid
= (max(locs
) + min(locs
)) / 2
3194 for node
in selection
:
3196 node
.location
.x
+= (mid_x
- new_mid
)
3198 node
.location
.y
+= (mid_y
- new_mid
)
3203 class NWSelectParentChildren(Operator
, NWBase
):
3204 bl_idname
= "node.nw_select_parent_child"
3205 bl_label
= "Select Parent or Children"
3206 bl_options
= {'REGISTER', 'UNDO'}
3208 option
: EnumProperty(
3211 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3212 ('CHILD', 'Select Children', 'Select members of selected frame'),
3216 def execute(self
, context
):
3217 nodes
, links
= get_nodes_links(context
)
3218 option
= self
.option
3219 selected
= [node
for node
in nodes
if node
.select
]
3220 if option
== 'PARENT':
3221 for sel
in selected
:
3224 parent
.select
= True
3225 else: # option == 'CHILD'
3226 for sel
in selected
:
3227 children
= [node
for node
in nodes
if node
.parent
== sel
]
3228 for kid
in children
:
3234 class NWDetachOutputs(Operator
, NWBase
):
3235 """Detach outputs of selected node leaving inputs linked"""
3236 bl_idname
= "node.nw_detach_outputs"
3237 bl_label
= "Detach Outputs"
3238 bl_options
= {'REGISTER', 'UNDO'}
3240 def execute(self
, context
):
3241 nodes
, links
= get_nodes_links(context
)
3242 selected
= context
.selected_nodes
3243 bpy
.ops
.node
.duplicate_move_keep_inputs()
3244 new_nodes
= context
.selected_nodes
3245 bpy
.ops
.node
.select_all(action
="DESELECT")
3246 for node
in selected
:
3248 bpy
.ops
.node
.delete_reconnect()
3249 for new_node
in new_nodes
:
3250 new_node
.select
= True
3251 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3256 class NWLinkToOutputNode(Operator
):
3257 """Link to Composite node or Material Output node"""
3258 bl_idname
= "node.nw_link_out"
3259 bl_label
= "Connect to Output"
3260 bl_options
= {'REGISTER', 'UNDO'}
3263 def poll(cls
, context
):
3265 if nw_check(context
):
3266 if context
.active_node
is not None:
3267 for out
in context
.active_node
.outputs
:
3268 if is_visible_socket(out
):
3273 def execute(self
, context
):
3274 nodes
, links
= get_nodes_links(context
)
3275 active
= nodes
.active
3277 tree_type
= context
.space_data
.tree_type
3278 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
3279 'WORLD': 'ShaderNodeOutputWorld',
3280 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
3282 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
3283 'CompositorNodeTree': 'CompositorNodeComposite',
3284 'TextureNodeTree': 'TextureNodeOutput',
3285 'GeometryNodeTree': 'NodeGroupOutput',
3288 # check whether the node is an output node and,
3289 # if supported, whether it's the active one
3290 if node
.rna_type
.identifier
== output_type \
3291 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
3295 else: # No output node exists
3296 bpy
.ops
.node
.select_all(action
="DESELECT")
3297 output_node
= nodes
.new(output_type
)
3298 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3299 output_node
.location
.y
= active
.location
.y
3302 for i
, output
in enumerate(active
.outputs
):
3303 if is_visible_socket(output
):
3306 for i
, output
in enumerate(active
.outputs
):
3307 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3312 if tree_type
== 'ShaderNodeTree':
3313 if active
.outputs
[output_index
].name
== 'Volume':
3315 elif active
.outputs
[output_index
].name
== 'Displacement':
3317 elif tree_type
== 'GeometryNodeTree':
3318 if active
.outputs
[output_index
].type != 'GEOMETRY':
3319 return {'CANCELLED'}
3320 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3322 force_update(context
) # viewport render does not update
3327 class NWMakeLink(Operator
, NWBase
):
3328 """Make a link from one socket to another"""
3329 bl_idname
= 'node.nw_make_link'
3330 bl_label
= 'Make Link'
3331 bl_options
= {'REGISTER', 'UNDO'}
3332 from_socket
: IntProperty()
3333 to_socket
: IntProperty()
3335 def execute(self
, context
):
3336 nodes
, links
= get_nodes_links(context
)
3338 n1
= nodes
[context
.scene
.NWLazySource
]
3339 n2
= nodes
[context
.scene
.NWLazyTarget
]
3341 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3343 force_update(context
)
3348 class NWCallInputsMenu(Operator
, NWBase
):
3349 """Link from this output"""
3350 bl_idname
= 'node.nw_call_inputs_menu'
3351 bl_label
= 'Make Link'
3352 bl_options
= {'REGISTER', 'UNDO'}
3353 from_socket
: IntProperty()
3355 def execute(self
, context
):
3356 nodes
, links
= get_nodes_links(context
)
3358 context
.scene
.NWSourceSocket
= self
.from_socket
3360 n1
= nodes
[context
.scene
.NWLazySource
]
3361 n2
= nodes
[context
.scene
.NWLazyTarget
]
3362 if len(n2
.inputs
) > 1:
3363 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3364 elif len(n2
.inputs
) == 1:
3365 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3369 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3370 """Add an Image Sequence"""
3371 bl_idname
= 'node.nw_add_sequence'
3372 bl_label
= 'Import Image Sequence'
3373 bl_options
= {'REGISTER', 'UNDO'}
3375 directory
: StringProperty(
3378 filename
: StringProperty(
3381 files
: CollectionProperty(
3382 type=bpy
.types
.OperatorFileListElement
,
3383 options
={'HIDDEN', 'SKIP_SAVE'}
3385 relative_path
: BoolProperty(
3386 name
='Relative Path',
3387 description
='Set the file path relative to the blend file, when possible',
3391 def draw(self
, context
):
3392 layout
= self
.layout
3393 layout
.alignment
= 'LEFT'
3395 layout
.prop(self
, 'relative_path')
3397 def execute(self
, context
):
3398 nodes
, links
= get_nodes_links(context
)
3399 directory
= self
.directory
3400 filename
= self
.filename
3402 tree
= context
.space_data
.node_tree
3405 # print ("\nDIR:", directory)
3406 # print ("FN:", filename)
3407 # print ("Fs:", list(f.name for f in files), '\n')
3409 if tree
.type == 'SHADER':
3410 node_type
= "ShaderNodeTexImage"
3411 elif tree
.type == 'COMPOSITING':
3412 node_type
= "CompositorNodeImage"
3414 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3415 return {'CANCELLED'}
3417 if not files
[0].name
and not filename
:
3418 self
.report({'ERROR'}, "No file chosen")
3419 return {'CANCELLED'}
3420 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3421 # User has selected multiple files without an active one, or the active one is non-existant
3422 filename
= files
[0].name
3424 if not path
.exists(directory
+filename
):
3425 self
.report({'ERROR'}, filename
+" does not exist!")
3426 return {'CANCELLED'}
3428 without_ext
= '.'.join(filename
.split('.')[:-1])
3430 # if last digit isn't a number, it's not a sequence
3431 if not without_ext
[-1].isdigit():
3432 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3433 return {'CANCELLED'}
3436 extension
= filename
.split('.')[-1]
3437 reverse
= without_ext
[::-1] # reverse string
3440 for char
in reverse
:
3446 without_num
= without_ext
[:count_numbers
*-1]
3448 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3450 num_frames
= len(files
)
3452 nodes_list
= [node
for node
in nodes
]
3454 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3455 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3459 yloc
+= node_mid_pt(node
, 'y')
3460 yloc
= yloc
/len(nodes
)
3465 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3467 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3469 node
.label
= name_with_hashes
3471 filepath
= directory
+(without_ext
+'.'+extension
)
3472 if self
.relative_path
:
3473 if bpy
.data
.filepath
:
3475 filepath
= bpy
.path
.relpath(filepath
)
3479 img
= bpy
.data
.images
.load(filepath
)
3480 img
.source
= 'SEQUENCE'
3481 img
.name
= name_with_hashes
3483 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3484 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
3485 image_user
.frame_duration
= num_frames
3490 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3491 """Add multiple images at once"""
3492 bl_idname
= 'node.nw_add_multiple_images'
3493 bl_label
= 'Open Selected Images'
3494 bl_options
= {'REGISTER', 'UNDO'}
3495 directory
: StringProperty(
3498 files
: CollectionProperty(
3499 type=bpy
.types
.OperatorFileListElement
,
3500 options
={'HIDDEN', 'SKIP_SAVE'}
3503 def execute(self
, context
):
3504 nodes
, links
= get_nodes_links(context
)
3506 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3508 if context
.space_data
.node_tree
.type == 'SHADER':
3509 node_type
= "ShaderNodeTexImage"
3510 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3511 node_type
= "CompositorNodeImage"
3513 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3514 return {'CANCELLED'}
3517 for f
in self
.files
:
3520 node
= nodes
.new(node_type
)
3521 new_nodes
.append(node
)
3524 node
.width_hidden
= 100
3525 node
.location
.x
= xloc
3526 node
.location
.y
= yloc
3529 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3532 # shift new nodes up to center of tree
3533 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3535 if node
in new_nodes
:
3537 node
.location
.y
+= (list_size
/2)
3543 class NWViewerFocus(bpy
.types
.Operator
):
3544 """Set the viewer tile center to the mouse position"""
3545 bl_idname
= "node.nw_viewer_focus"
3546 bl_label
= "Viewer Focus"
3548 x
: bpy
.props
.IntProperty()
3549 y
: bpy
.props
.IntProperty()
3552 def poll(cls
, context
):
3553 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
3555 def execute(self
, context
):
3558 def invoke(self
, context
, event
):
3559 render
= context
.scene
.render
3560 space
= context
.space_data
3561 percent
= render
.resolution_percentage
*0.01
3563 nodes
, links
= get_nodes_links(context
)
3564 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
3567 mlocx
= event
.mouse_region_x
3568 mlocy
= event
.mouse_region_y
3569 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
3571 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
3572 region_x
= context
.region
.width
3573 region_y
= context
.region
.height
3575 region_center_x
= context
.region
.width
/ 2
3576 region_center_y
= context
.region
.height
/ 2
3578 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
3579 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
3581 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
3582 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
3584 margin_x
= region_center_x
- backdrop_center_x
3585 margin_y
= region_center_y
- backdrop_center_y
3587 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
3588 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
3590 for node
in viewers
:
3591 node
.center_x
= abs_mouse_x
3592 node
.center_y
= abs_mouse_y
3594 return {'PASS_THROUGH'}
3596 return self
.execute(context
)
3599 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
3600 """Save the current viewer node to an image file"""
3601 bl_idname
= "node.nw_save_viewer"
3602 bl_label
= "Save This Image"
3603 filepath
: StringProperty(subtype
="FILE_PATH")
3604 filename_ext
: EnumProperty(
3606 description
="Choose the file format to save to",
3607 items
=(('.bmp', "BMP", ""),
3608 ('.rgb', 'IRIS', ""),
3609 ('.png', 'PNG', ""),
3610 ('.jpg', 'JPEG', ""),
3611 ('.jp2', 'JPEG2000', ""),
3612 ('.tga', 'TARGA', ""),
3613 ('.cin', 'CINEON', ""),
3614 ('.dpx', 'DPX', ""),
3615 ('.exr', 'OPEN_EXR', ""),
3616 ('.hdr', 'HDR', ""),
3617 ('.tif', 'TIFF', "")),
3622 def poll(cls
, context
):
3624 if nw_check(context
):
3625 if context
.space_data
.tree_type
== 'CompositorNodeTree':
3626 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
3627 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
3631 def execute(self
, context
):
3648 basename
, ext
= path
.splitext(fp
)
3649 old_render_format
= context
.scene
.render
.image_settings
.file_format
3650 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
3651 context
.area
.type = "IMAGE_EDITOR"
3652 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
3653 context
.area
.spaces
[0].image
.save_render(fp
)
3654 context
.area
.type = "NODE_EDITOR"
3655 context
.scene
.render
.image_settings
.file_format
= old_render_format
3659 class NWResetNodes(bpy
.types
.Operator
):
3660 """Reset Nodes in Selection"""
3661 bl_idname
= "node.nw_reset_nodes"
3662 bl_label
= "Reset Nodes"
3663 bl_options
= {'REGISTER', 'UNDO'}
3666 def poll(cls
, context
):
3667 space
= context
.space_data
3668 return space
.type == 'NODE_EDITOR'
3670 def execute(self
, context
):
3671 node_active
= context
.active_node
3672 node_selected
= context
.selected_nodes
3673 node_ignore
= ["FRAME","REROUTE", "GROUP"]
3675 # Check if one node is selected at least
3676 if not (len(node_selected
) > 0):
3677 self
.report({'ERROR'}, "1 node must be selected at least")
3678 return {'CANCELLED'}
3680 active_node_name
= node_active
.name
if node_active
.select
else None
3681 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
3683 # Create output lists
3684 selected_node_names
= [n
.name
for n
in node_selected
]
3687 # Reset all valid children in a frame
3688 node_active_is_frame
= False
3689 if len(node_selected
) == 1 and node_active
.type == "FRAME":
3690 node_tree
= node_active
.id_data
3691 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
3693 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
3694 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
3695 node_active_is_frame
= True
3697 # Check if valid nodes in selection
3698 if not (len(valid_nodes
) > 0):
3699 # Check for frames only
3700 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
3701 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
3702 self
.report({'ERROR'}, "Please select only 1 frame to reset")
3704 self
.report({'ERROR'}, "No valid node(s) in selection")
3705 return {'CANCELLED'}
3707 # Report nodes that are not valid
3708 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
3709 valid_node_names
= [n
.name
for n
in valid_nodes
]
3710 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
3711 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
3713 # Deselect all nodes
3714 for i
in node_selected
:
3717 # Run through all valid nodes
3718 for node
in valid_nodes
:
3720 parent
= node
.parent
if node
.parent
else None
3721 node_loc
= [node
.location
.x
, node
.location
.y
]
3723 node_tree
= node
.id_data
3724 props_to_copy
= 'bl_idname name location height width'.split(' ')
3727 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
3728 for i
in (i
for i
in mappings
if i
.is_linked
):
3730 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
3732 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
3734 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
3735 props_to_copy
.pop(0)
3737 for prop
in props_to_copy
:
3738 setattr(new_node
, prop
, props
[prop
])
3740 nodes
= node_tree
.nodes
3742 new_node
.name
= props
['name']
3745 new_node
.parent
= parent
3746 new_node
.location
= node_loc
3748 for str_from
, str_to
in reconnections
:
3749 node_tree
.links
.new(eval(str_from
), eval(str_to
))
3751 new_node
.select
= False
3752 success_names
.append(new_node
.name
)
3754 # Reselect all nodes
3755 if selected_node_names
and node_active_is_frame
is False:
3756 for i
in selected_node_names
:
3757 node_tree
.nodes
[i
].select
= True
3759 if active_node_name
is not None:
3760 node_tree
.nodes
[active_node_name
].select
= True
3761 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
3763 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
3771 def drawlayout(context
, layout
, mode
='non-panel'):
3772 tree_type
= context
.space_data
.tree_type
3774 col
= layout
.column(align
=True)
3775 col
.menu(NWMergeNodesMenu
.bl_idname
)
3778 col
= layout
.column(align
=True)
3779 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
3782 if tree_type
== 'ShaderNodeTree':
3783 col
= layout
.column(align
=True)
3784 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
3785 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
3788 col
= layout
.column(align
=True)
3789 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
3790 col
.operator(NWSwapLinks
.bl_idname
)
3791 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
3794 col
= layout
.column(align
=True)
3795 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
3796 if tree_type
!= 'GeometryNodeTree':
3797 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
3800 col
= layout
.column(align
=True)
3802 row
= col
.row(align
=True)
3803 row
.operator(NWClearLabel
.bl_idname
).option
= True
3804 row
.operator(NWModifyLabels
.bl_idname
)
3806 col
.operator(NWClearLabel
.bl_idname
).option
= True
3807 col
.operator(NWModifyLabels
.bl_idname
)
3808 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
3810 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
3813 col
= layout
.column(align
=True)
3814 if tree_type
== 'CompositorNodeTree':
3815 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
3816 if tree_type
!= 'GeometryNodeTree':
3817 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
3820 col
= layout
.column(align
=True)
3821 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
3824 col
= layout
.column(align
=True)
3825 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
3828 col
= layout
.column(align
=True)
3829 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
3833 class NodeWranglerPanel(Panel
, NWBase
):
3834 bl_idname
= "NODE_PT_nw_node_wrangler"
3835 bl_space_type
= 'NODE_EDITOR'
3836 bl_label
= "Node Wrangler"
3837 bl_region_type
= "UI"
3838 bl_category
= "Node Wrangler"
3840 prepend
: StringProperty(
3843 append
: StringProperty()
3844 remove
: StringProperty()
3846 def draw(self
, context
):
3847 self
.layout
.label(text
="(Quick access: Shift+W)")
3848 drawlayout(context
, self
.layout
, mode
='panel')
3854 class NodeWranglerMenu(Menu
, NWBase
):
3855 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
3856 bl_label
= "Node Wrangler"
3858 def draw(self
, context
):
3859 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
3860 drawlayout(context
, self
.layout
)
3863 class NWMergeNodesMenu(Menu
, NWBase
):
3864 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
3865 bl_label
= "Merge Selected Nodes"
3867 def draw(self
, context
):
3868 type = context
.space_data
.tree_type
3869 layout
= self
.layout
3870 if type == 'ShaderNodeTree':
3871 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
3872 if type == 'GeometryNodeTree':
3873 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
3874 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
3876 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
3877 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
3878 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
3880 props
.merge_type
= 'ZCOMBINE'
3881 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
3883 props
.merge_type
= 'ALPHAOVER'
3885 class NWMergeGeometryMenu(Menu
, NWBase
):
3886 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
3887 bl_label
= "Merge Selected Nodes using Geometry Nodes"
3888 def draw(self
, context
):
3889 layout
= self
.layout
3890 # The boolean node + Join Geometry node
3891 for type, name
, description
in geo_combine_operations
:
3892 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3894 props
.merge_type
= 'GEOMETRY'
3896 class NWMergeShadersMenu(Menu
, NWBase
):
3897 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
3898 bl_label
= "Merge Selected Nodes using Shaders"
3900 def draw(self
, context
):
3901 layout
= self
.layout
3902 for type in ('MIX', 'ADD'):
3903 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
3905 props
.merge_type
= 'SHADER'
3908 class NWMergeMixMenu(Menu
, NWBase
):
3909 bl_idname
= "NODE_MT_nw_merge_mix_menu"
3910 bl_label
= "Merge Selected Nodes using Mix"
3912 def draw(self
, context
):
3913 layout
= self
.layout
3914 for type, name
, description
in blend_types
:
3915 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3917 props
.merge_type
= 'MIX'
3920 class NWConnectionListOutputs(Menu
, NWBase
):
3921 bl_idname
= "NODE_MT_nw_connection_list_out"
3924 def draw(self
, context
):
3925 layout
= self
.layout
3926 nodes
, links
= get_nodes_links(context
)
3928 n1
= nodes
[context
.scene
.NWLazySource
]
3929 for index
, output
in enumerate(n1
.outputs
):
3930 # Only show sockets that are exposed.
3932 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=output
.name
, icon
="RADIOBUT_OFF").from_socket
=index
3935 class NWConnectionListInputs(Menu
, NWBase
):
3936 bl_idname
= "NODE_MT_nw_connection_list_in"
3939 def draw(self
, context
):
3940 layout
= self
.layout
3941 nodes
, links
= get_nodes_links(context
)
3943 n2
= nodes
[context
.scene
.NWLazyTarget
]
3945 for index
, input in enumerate(n2
.inputs
):
3946 # Only show sockets that are exposed.
3947 # This prevents, for example, the scale value socket
3948 # of the vector math node being added to the list when
3949 # the mode is not 'SCALE'.
3951 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=input.name
, icon
="FORWARD")
3952 op
.from_socket
= context
.scene
.NWSourceSocket
3953 op
.to_socket
= index
3956 class NWMergeMathMenu(Menu
, NWBase
):
3957 bl_idname
= "NODE_MT_nw_merge_math_menu"
3958 bl_label
= "Merge Selected Nodes using Math"
3960 def draw(self
, context
):
3961 layout
= self
.layout
3962 for type, name
, description
in operations
:
3963 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3965 props
.merge_type
= 'MATH'
3968 class NWBatchChangeNodesMenu(Menu
, NWBase
):
3969 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
3970 bl_label
= "Batch Change Selected Nodes"
3972 def draw(self
, context
):
3973 layout
= self
.layout
3974 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
3975 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
3978 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
3979 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
3980 bl_label
= "Batch Change Blend Type"
3982 def draw(self
, context
):
3983 layout
= self
.layout
3984 for type, name
, description
in blend_types
:
3985 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3986 props
.blend_type
= type
3987 props
.operation
= 'CURRENT'
3990 class NWBatchChangeOperationMenu(Menu
, NWBase
):
3991 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
3992 bl_label
= "Batch Change Math Operation"
3994 def draw(self
, context
):
3995 layout
= self
.layout
3996 for type, name
, description
in operations
:
3997 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3998 props
.blend_type
= 'CURRENT'
3999 props
.operation
= type
4002 class NWCopyToSelectedMenu(Menu
, NWBase
):
4003 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4004 bl_label
= "Copy to Selected"
4006 def draw(self
, context
):
4007 layout
= self
.layout
4008 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4009 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4012 class NWCopyLabelMenu(Menu
, NWBase
):
4013 bl_idname
= "NODE_MT_nw_copy_label_menu"
4014 bl_label
= "Copy Label"
4016 def draw(self
, context
):
4017 layout
= self
.layout
4018 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4019 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4020 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4023 class NWAddReroutesMenu(Menu
, NWBase
):
4024 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4025 bl_label
= "Add Reroutes"
4026 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4028 def draw(self
, context
):
4029 layout
= self
.layout
4030 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4031 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4032 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4035 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4036 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4037 bl_label
= "Link Active to Selected"
4039 def draw(self
, context
):
4040 layout
= self
.layout
4041 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4042 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4043 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4046 class NWLinkStandardMenu(Menu
, NWBase
):
4047 bl_idname
= "NODE_MT_nw_link_standard_menu"
4048 bl_label
= "To All Selected"
4050 def draw(self
, context
):
4051 layout
= self
.layout
4052 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4053 props
.replace
= False
4054 props
.use_node_name
= False
4055 props
.use_outputs_names
= False
4056 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4057 props
.replace
= True
4058 props
.use_node_name
= False
4059 props
.use_outputs_names
= False
4062 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4063 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4064 bl_label
= "Use Node Name/Label"
4066 def draw(self
, context
):
4067 layout
= self
.layout
4068 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4069 props
.replace
= False
4070 props
.use_node_name
= True
4071 props
.use_outputs_names
= False
4072 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4073 props
.replace
= True
4074 props
.use_node_name
= True
4075 props
.use_outputs_names
= False
4078 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4079 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4080 bl_label
= "Use Outputs Names"
4082 def draw(self
, context
):
4083 layout
= self
.layout
4084 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4085 props
.replace
= False
4086 props
.use_node_name
= False
4087 props
.use_outputs_names
= True
4088 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4089 props
.replace
= True
4090 props
.use_node_name
= False
4091 props
.use_outputs_names
= True
4094 class NWAttributeMenu(bpy
.types
.Menu
):
4095 bl_idname
= "NODE_MT_nw_node_attribute_menu"
4096 bl_label
= "Attributes"
4099 def poll(cls
, context
):
4101 if nw_check(context
):
4102 snode
= context
.space_data
4103 valid
= snode
.tree_type
== 'ShaderNodeTree'
4106 def draw(self
, context
):
4108 nodes
, links
= get_nodes_links(context
)
4109 mat
= context
.object.active_material
4112 for obj
in bpy
.data
.objects
:
4113 for slot
in obj
.material_slots
:
4114 if slot
.material
== mat
:
4118 if obj
.data
.attributes
:
4119 for attr
in obj
.data
.attributes
:
4120 attrs
.append(attr
.name
)
4121 attrs
= list(set(attrs
)) # get a unique list
4125 l
.operator(NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
4127 l
.label(text
="No attributes on objects with this material")
4130 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4131 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4132 bl_label
= "Switch Type to..."
4134 def draw(self
, context
):
4135 layout
= self
.layout
4136 categories
= [c
for c
in node_categories_iter(context
)
4137 if c
.name
not in ['Group', 'Script']]
4138 for cat
in categories
:
4139 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4140 if hasattr(bpy
.types
, idname
):
4143 layout
.label(text
="Unable to load altered node lists.")
4144 layout
.label(text
="Please re-enable Node Wrangler.")
4148 def draw_switch_category_submenu(self
, context
):
4149 layout
= self
.layout
4150 if self
.category
.name
== 'Layout':
4151 for node
in self
.category
.items(context
):
4152 if node
.nodetype
!= 'NodeFrame':
4153 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4154 props
.to_type
= node
.nodetype
4156 for node
in self
.category
.items(context
):
4157 if isinstance(node
, NodeItemCustom
):
4158 node
.draw(self
, layout
, context
)
4160 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4161 props
.to_type
= node
.nodetype
4164 # APPENDAGES TO EXISTING UI
4168 def select_parent_children_buttons(self
, context
):
4169 layout
= self
.layout
4170 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
4171 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
4174 def attr_nodes_menu_func(self
, context
):
4175 col
= self
.layout
.column(align
=True)
4176 col
.menu("NODE_MT_nw_node_attribute_menu")
4180 def multipleimages_menu_func(self
, context
):
4181 col
= self
.layout
.column(align
=True)
4182 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
4183 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
4187 def bgreset_menu_func(self
, context
):
4188 self
.layout
.operator(NWResetBG
.bl_idname
)
4191 def save_viewer_menu_func(self
, context
):
4192 if nw_check(context
):
4193 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4194 if context
.scene
.node_tree
.nodes
.active
:
4195 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
4196 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
4199 def reset_nodes_button(self
, context
):
4200 node_active
= context
.active_node
4201 node_selected
= context
.selected_nodes
4202 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4204 # Check if active node is in the selection and respective type
4205 if (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type not in node_ignore
:
4206 row
= self
.layout
.row()
4207 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
4208 self
.layout
.separator()
4210 elif (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type == "FRAME":
4211 row
= self
.layout
.row()
4212 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
4213 self
.layout
.separator()
4217 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4219 switch_category_menus
= []
4221 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4222 # props entry: (property name, property value)
4225 # NWMergeNodes with Ctrl (AUTO).
4226 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
4227 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4228 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
4229 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4230 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4231 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4232 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
4233 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4234 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4235 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4236 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
4237 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4238 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4239 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4240 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
4241 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4242 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4243 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4244 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
4245 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4246 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
4247 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4248 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
4249 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4250 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4251 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4252 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4253 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
4254 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4255 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
4256 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4257 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4258 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4259 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
4260 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4261 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4262 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4263 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
4264 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4265 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4266 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4267 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
4268 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4269 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4270 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4271 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
4272 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4273 # NWMergeNodes with Ctrl Shift (MATH)
4274 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4275 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4276 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
4277 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4278 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4279 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4280 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
4281 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4282 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4283 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4284 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
4285 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4286 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4287 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4288 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
4289 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4290 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
4291 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4292 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
4293 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4294 # BATCH CHANGE NODES
4295 # NWBatchChangeNodes with Alt
4296 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
4297 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4298 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
4299 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4300 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4301 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4302 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
4303 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4304 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4305 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4306 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
4307 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4308 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4309 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4310 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
4311 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4312 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4313 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4314 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
4315 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4316 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
4317 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4318 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
4319 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4320 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
4321 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4322 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
4323 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4324 # LINK ACTIVE TO SELECTED
4325 # Don't use names, don't replace links (K)
4326 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
4327 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4328 # Don't use names, replace links (Shift K)
4329 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
4330 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4331 # Use node name, don't replace links (')
4332 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
4333 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4334 # Use node name, replace links (Shift ')
4335 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
4336 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4337 # Don't use names, don't replace links (;)
4338 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
4339 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4340 # Don't use names, replace links (')
4341 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
4342 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4344 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4345 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4346 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4347 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4348 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4349 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4350 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4351 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4352 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4353 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4354 # CLEAR LABEL (Alt L)
4355 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4356 # MODIFY LABEL (Alt Shift L)
4357 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4358 # Copy Label from active to selected
4359 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4360 # DETACH OUTPUTS (Alt Shift D)
4361 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4362 # LINK TO OUTPUT NODE (O)
4363 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4364 # SELECT PARENT/CHILDREN
4366 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4368 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4370 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4371 # Add Principled BSDF Texture Setup
4372 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4374 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4376 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4378 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4380 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Links"),
4382 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
4383 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
4385 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
4387 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4389 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
4390 # Lazy Connect with Menu
4391 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4392 # Viewer Tile Center
4393 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4395 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4396 # Reset Nodes (Back Space)
4397 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4399 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
4400 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4401 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4402 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
4403 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
4404 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
4409 NWPrincipledPreferences
,
4429 NWAddPrincipledSetup
,
4431 NWLinkActiveToSelected
,
4433 NWSelectParentChildren
,
4439 NWAddMultipleImages
,
4447 NWMergeGeometryMenu
,
4449 NWConnectionListOutputs
,
4450 NWConnectionListInputs
,
4452 NWBatchChangeNodesMenu
,
4453 NWBatchChangeBlendTypeMenu
,
4454 NWBatchChangeOperationMenu
,
4455 NWCopyToSelectedMenu
,
4458 NWLinkActiveToSelectedMenu
,
4460 NWLinkUseNodeNameMenu
,
4461 NWLinkUseOutputsNamesMenu
,
4463 NWSwitchNodeTypeMenu
,
4467 from bpy
.utils
import register_class
4470 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
4471 name
="Busy Drawing!",
4473 description
="An internal property used to store only the first mouse position")
4474 bpy
.types
.Scene
.NWLazySource
= StringProperty(
4475 name
="Lazy Source!",
4477 description
="An internal property used to store the first node in a Lazy Connect operation")
4478 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
4479 name
="Lazy Target!",
4481 description
="An internal property used to store the last node in a Lazy Connect operation")
4482 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
4483 name
="Source Socket!",
4485 description
="An internal property used to store the source socket in a Lazy Connect operation")
4486 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
4489 description
="An internal property used to determine if a socket is generated by the addon"
4496 addon_keymaps
.clear()
4497 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
4499 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
4500 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
4501 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
4503 for prop
, value
in props
:
4504 setattr(kmi
.properties
, prop
, value
)
4505 addon_keymaps
.append((km
, kmi
))
4508 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
4509 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
4510 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
4511 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
4512 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
4513 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
4514 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
4515 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
4518 switch_category_menus
.clear()
4519 for cat
in node_categories_iter(None):
4520 if cat
.name
not in ['Group', 'Script']:
4521 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4522 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
4523 "bl_space_type": 'NODE_EDITOR',
4524 "bl_label": cat
.name
,
4527 "draw": draw_switch_category_submenu
,
4530 switch_category_menus
.append(switch_category_type
)
4532 bpy
.utils
.register_class(switch_category_type
)
4536 from bpy
.utils
import unregister_class
4539 del bpy
.types
.Scene
.NWBusyDrawing
4540 del bpy
.types
.Scene
.NWLazySource
4541 del bpy
.types
.Scene
.NWLazyTarget
4542 del bpy
.types
.Scene
.NWSourceSocket
4543 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
4545 for cat_types
in switch_category_menus
:
4546 bpy
.utils
.unregister_class(cat_types
)
4547 switch_category_menus
.clear()
4550 for km
, kmi
in addon_keymaps
:
4551 km
.keymap_items
.remove(kmi
)
4552 addon_keymaps
.clear()
4555 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
4556 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
4557 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
4558 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
4559 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
4560 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
4561 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
4562 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
4565 unregister_class(cls
)
4567 if __name__
== "__main__":