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
/ 72
272 def prefs_line_width():
273 prefs
= bpy
.context
.preferences
.system
274 return prefs
.pixel_size
277 def node_mid_pt(node
, axis
):
279 d
= node
.location
.x
+ (node
.dimensions
.x
/ 2)
281 d
= node
.location
.y
- (node
.dimensions
.y
/ 2)
287 def autolink(node1
, node2
, links
):
289 available_inputs
= [inp
for inp
in node2
.inputs
if inp
.enabled
]
290 available_outputs
= [outp
for outp
in node1
.outputs
if outp
.enabled
]
291 for outp
in available_outputs
:
292 for inp
in available_inputs
:
293 if not inp
.is_linked
and inp
.name
== outp
.name
:
298 for outp
in available_outputs
:
299 for inp
in available_inputs
:
300 if not inp
.is_linked
and inp
.type == outp
.type:
305 # force some connection even if the type doesn't match
306 if available_outputs
:
307 for inp
in available_inputs
:
308 if not inp
.is_linked
:
310 links
.new(available_outputs
[0], inp
)
313 # even if no sockets are open, force one of matching type
314 for outp
in available_outputs
:
315 for inp
in available_inputs
:
316 if inp
.type == outp
.type:
322 for outp
in available_outputs
:
323 for inp
in available_inputs
:
328 print("Could not make a link from " + node1
.name
+ " to " + node2
.name
)
331 def abs_node_location(node
):
332 abs_location
= node
.location
333 if node
.parent
is None:
335 return abs_location
+ abs_node_location(node
.parent
)
337 def node_at_pos(nodes
, context
, event
):
338 nodes_under_mouse
= []
341 store_mouse_cursor(context
, event
)
342 x
, y
= context
.space_data
.cursor_location
344 # Make a list of each corner (and middle of border) for each node.
345 # Will be sorted to find nearest point and thus nearest node
346 node_points_with_dist
= []
349 if node
.type != 'FRAME': # no point trying to link to a frame node
350 dimx
= node
.dimensions
.x
/ dpi_fac()
351 dimy
= node
.dimensions
.y
/ dpi_fac()
352 locx
, locy
= abs_node_location(node
)
355 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- locy
)]) # Top Left
356 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- locy
)]) # Top Right
357 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- dimy
))]) # Bottom Left
358 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- dimy
))]) # Bottom Right
360 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- locy
)]) # Mid Top
361 node_points_with_dist
.append([node
, hypot(x
- (locx
+ (dimx
/ 2)), y
- (locy
- dimy
))]) # Mid Bottom
362 node_points_with_dist
.append([node
, hypot(x
- locx
, y
- (locy
- (dimy
/ 2)))]) # Mid Left
363 node_points_with_dist
.append([node
, hypot(x
- (locx
+ dimx
), y
- (locy
- (dimy
/ 2)))]) # Mid Right
365 nearest_node
= sorted(node_points_with_dist
, key
=lambda k
: k
[1])[0][0]
368 if node
.type != 'FRAME' and skipnode
== False:
369 locx
, locy
= abs_node_location(node
)
370 dimx
= node
.dimensions
.x
/ dpi_fac()
371 dimy
= node
.dimensions
.y
/ dpi_fac()
372 if (locx
<= x
<= locx
+ dimx
) and \
373 (locy
- dimy
<= y
<= locy
):
374 nodes_under_mouse
.append(node
)
376 if len(nodes_under_mouse
) == 1:
377 if nodes_under_mouse
[0] != nearest_node
:
378 target_node
= nodes_under_mouse
[0] # use the node under the mouse if there is one and only one
380 target_node
= nearest_node
# else use the nearest node
382 target_node
= nearest_node
386 def store_mouse_cursor(context
, event
):
387 space
= context
.space_data
388 v2d
= context
.region
.view2d
389 tree
= space
.edit_tree
391 # convert mouse position to the View2D for later node placement
392 if context
.region
.type == 'WINDOW':
393 space
.cursor_location_from_region(event
.mouse_region_x
, event
.mouse_region_y
)
395 space
.cursor_location
= tree
.view_center
397 def draw_line(x1
, y1
, x2
, y2
, size
, colour
=(1.0, 1.0, 1.0, 0.7)):
398 shader
= gpu
.shader
.from_builtin('POLYLINE_SMOOTH_COLOR')
399 shader
.uniform_float("viewportSize", gpu
.state
.viewport_get()[2:])
400 shader
.uniform_float("lineWidth", size
* prefs_line_width())
402 vertices
= ((x1
, y1
), (x2
, y2
))
403 vertex_colors
= ((colour
[0]+(1.0-colour
[0])/4,
404 colour
[1]+(1.0-colour
[1])/4,
405 colour
[2]+(1.0-colour
[2])/4,
406 colour
[3]+(1.0-colour
[3])/4),
409 batch
= batch_for_shader(shader
, 'LINE_STRIP', {"pos": vertices
, "color": vertex_colors
})
413 def draw_circle_2d_filled(mx
, my
, radius
, colour
=(1.0, 1.0, 1.0, 0.7)):
414 radius
= radius
* prefs_line_width()
416 vertices
= [(radius
* cos(i
* 2 * pi
/ sides
) + mx
,
417 radius
* sin(i
* 2 * pi
/ sides
) + my
)
418 for i
in range(sides
+ 1)]
420 shader
= gpu
.shader
.from_builtin('UNIFORM_COLOR')
421 shader
.uniform_float("color", colour
)
422 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
426 def draw_rounded_node_border(node
, radius
=8, colour
=(1.0, 1.0, 1.0, 0.7)):
427 area_width
= bpy
.context
.area
.width
429 radius
*= prefs_line_width()
431 nlocx
, nlocy
= abs_node_location(node
)
433 nlocx
= (nlocx
+1) * dpi_fac()
434 nlocy
= (nlocy
+1) * dpi_fac()
435 ndimx
= node
.dimensions
.x
436 ndimy
= node
.dimensions
.y
441 if node
.type == 'REROUTE':
448 shader
= gpu
.shader
.from_builtin('UNIFORM_COLOR')
449 shader
.uniform_float("color", colour
)
452 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
454 for i
in range(sides
+1):
457 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
458 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
459 vertices
.append((cosine
,sine
))
461 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
465 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
467 for i
in range(sides
+1):
470 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
471 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
472 vertices
.append((cosine
,sine
))
474 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
478 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
480 for i
in range(sides
+1):
483 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
484 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
485 vertices
.append((cosine
,sine
))
487 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
490 # Bottom right corner
491 mx
, my
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
493 for i
in range(sides
+1):
496 cosine
= radius
* cos(i
* 2 * pi
/ sides
) + mx
497 sine
= radius
* sin(i
* 2 * pi
/ sides
) + my
498 vertices
.append((cosine
,sine
))
500 batch
= batch_for_shader(shader
, 'TRI_FAN', {"pos": vertices
})
503 # prepare drawing all edges in one batch
509 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
510 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
- ndimy
, clip
=False)
511 if m1x
< area_width
and m2x
< area_width
:
512 vertices
.extend([(m2x
-radius
,m2y
), (m2x
,m2y
),
513 (m1x
,m1y
), (m1x
-radius
,m1y
)])
514 indices
.extend([(id_last
, id_last
+1, id_last
+3),
515 (id_last
+3, id_last
+1, id_last
+2)])
519 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
, clip
=False)
520 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
521 m1x
= min(m1x
, area_width
)
522 m2x
= min(m2x
, area_width
)
523 vertices
.extend([(m1x
,m1y
), (m2x
,m1y
),
524 (m2x
,m1y
+radius
), (m1x
,m1y
+radius
)])
525 indices
.extend([(id_last
, id_last
+1, id_last
+3),
526 (id_last
+3, id_last
+1, id_last
+2)])
530 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
, clip
=False)
531 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
- ndimy
, clip
=False)
532 if m1x
< area_width
and m2x
< area_width
:
533 vertices
.extend([(m1x
,m2y
), (m1x
+radius
,m2y
),
534 (m1x
+radius
,m1y
), (m1x
,m1y
)])
535 indices
.extend([(id_last
, id_last
+1, id_last
+3),
536 (id_last
+3, id_last
+1, id_last
+2)])
540 m1x
, m1y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
, nlocy
-ndimy
, clip
=False)
541 m2x
, m2y
= bpy
.context
.region
.view2d
.view_to_region(nlocx
+ ndimx
, nlocy
-ndimy
, clip
=False)
542 m1x
= min(m1x
, area_width
)
543 m2x
= min(m2x
, area_width
)
544 vertices
.extend([(m1x
,m2y
), (m2x
,m2y
),
545 (m2x
,m1y
-radius
), (m1x
,m1y
-radius
)])
546 indices
.extend([(id_last
, id_last
+1, id_last
+3),
547 (id_last
+3, id_last
+1, id_last
+2)])
549 # now draw all edges in one batch
550 if len(vertices
) != 0:
551 batch
= batch_for_shader(shader
, 'TRIS', {"pos": vertices
}, indices
=indices
)
554 def draw_callback_nodeoutline(self
, context
, mode
):
556 gpu
.state
.blend_set('ALPHA')
558 nodes
, links
= get_nodes_links(context
)
561 col_outer
= (1.0, 0.2, 0.2, 0.4)
562 col_inner
= (0.0, 0.0, 0.0, 0.5)
563 col_circle_inner
= (0.3, 0.05, 0.05, 1.0)
564 elif mode
== "LINKMENU":
565 col_outer
= (0.4, 0.6, 1.0, 0.4)
566 col_inner
= (0.0, 0.0, 0.0, 0.5)
567 col_circle_inner
= (0.08, 0.15, .3, 1.0)
569 col_outer
= (0.2, 1.0, 0.2, 0.4)
570 col_inner
= (0.0, 0.0, 0.0, 0.5)
571 col_circle_inner
= (0.05, 0.3, 0.05, 1.0)
573 m1x
= self
.mouse_path
[0][0]
574 m1y
= self
.mouse_path
[0][1]
575 m2x
= self
.mouse_path
[-1][0]
576 m2y
= self
.mouse_path
[-1][1]
578 n1
= nodes
[context
.scene
.NWLazySource
]
579 n2
= nodes
[context
.scene
.NWLazyTarget
]
582 col_outer
= (0.4, 0.4, 0.4, 0.4)
583 col_inner
= (0.0, 0.0, 0.0, 0.5)
584 col_circle_inner
= (0.2, 0.2, 0.2, 1.0)
586 draw_rounded_node_border(n1
, radius
=6, colour
=col_outer
) # outline
587 draw_rounded_node_border(n1
, radius
=5, colour
=col_inner
) # inner
588 draw_rounded_node_border(n2
, radius
=6, colour
=col_outer
) # outline
589 draw_rounded_node_border(n2
, radius
=5, colour
=col_inner
) # inner
591 draw_line(m1x
, m1y
, m2x
, m2y
, 5, col_outer
) # line outline
592 draw_line(m1x
, m1y
, m2x
, m2y
, 2, col_inner
) # line inner
595 draw_circle_2d_filled(m1x
, m1y
, 7, col_outer
)
596 draw_circle_2d_filled(m2x
, m2y
, 7, col_outer
)
599 draw_circle_2d_filled(m1x
, m1y
, 5, col_circle_inner
)
600 draw_circle_2d_filled(m2x
, m2y
, 5, col_circle_inner
)
602 gpu
.state
.blend_set('NONE')
604 def get_active_tree(context
):
605 tree
= context
.space_data
.node_tree
607 # Get nodes from currently edited tree.
608 # If user is editing a group, space_data.node_tree is still the base level (outside group).
609 # context.active_node is in the group though, so if space_data.node_tree.nodes.active is not
610 # the same as context.active_node, the user is in a group.
611 # Check recursively until we find the real active node_tree:
612 if tree
.nodes
.active
:
613 while tree
.nodes
.active
!= context
.active_node
:
614 tree
= tree
.nodes
.active
.node_tree
618 def get_nodes_links(context
):
619 tree
, path
= get_active_tree(context
)
620 return tree
.nodes
, tree
.links
622 def is_viewer_socket(socket
):
623 # checks if a internal socket is a valid viewer socket
624 return socket
.name
== viewer_socket_name
and socket
.NWViewerSocket
626 def get_internal_socket(socket
):
627 #get the internal socket from a socket inside or outside the group
629 if node
.type == 'GROUP_OUTPUT':
630 source_iterator
= node
.inputs
631 iterator
= node
.id_data
.outputs
632 elif node
.type == 'GROUP_INPUT':
633 source_iterator
= node
.outputs
634 iterator
= node
.id_data
.inputs
635 elif hasattr(node
, "node_tree"):
637 source_iterator
= node
.outputs
638 iterator
= node
.node_tree
.outputs
640 source_iterator
= node
.inputs
641 iterator
= node
.node_tree
.inputs
645 for i
, s
in enumerate(source_iterator
):
650 def is_viewer_link(link
, output_node
):
651 if link
.to_node
== output_node
and link
.to_socket
== output_node
.inputs
[0]:
653 if link
.to_node
.type == 'GROUP_OUTPUT':
654 socket
= get_internal_socket(link
.to_socket
)
655 if is_viewer_socket(socket
):
659 def get_group_output_node(tree
):
660 for node
in tree
.nodes
:
661 if node
.type == 'GROUP_OUTPUT' and node
.is_active_output
== True:
664 def get_output_location(tree
):
665 # get right-most location
666 sorted_by_xloc
= (sorted(tree
.nodes
, key
=lambda x
: x
.location
.x
))
667 max_xloc_node
= sorted_by_xloc
[-1]
669 # get average y location
671 for node
in tree
.nodes
:
672 sum_yloc
+= node
.location
.y
674 loc_x
= max_xloc_node
.location
.x
+ max_xloc_node
.dimensions
.x
+ 80
675 loc_y
= sum_yloc
/ len(tree
.nodes
)
679 class NWPrincipledPreferences(bpy
.types
.PropertyGroup
):
680 base_color
: StringProperty(
682 default
='diffuse diff albedo base col color',
683 description
='Naming Components for Base Color maps')
684 sss_color
: StringProperty(
685 name
='Subsurface Color',
686 default
='sss subsurface',
687 description
='Naming Components for Subsurface Color maps')
688 metallic
: StringProperty(
690 default
='metallic metalness metal mtl',
691 description
='Naming Components for metallness maps')
692 specular
: StringProperty(
694 default
='specularity specular spec spc',
695 description
='Naming Components for Specular maps')
696 normal
: StringProperty(
698 default
='normal nor nrm nrml norm',
699 description
='Naming Components for Normal maps')
700 bump
: StringProperty(
703 description
='Naming Components for bump maps')
704 rough
: StringProperty(
706 default
='roughness rough rgh',
707 description
='Naming Components for roughness maps')
708 gloss
: StringProperty(
710 default
='gloss glossy glossiness',
711 description
='Naming Components for glossy maps')
712 displacement
: StringProperty(
714 default
='displacement displace disp dsp height heightmap',
715 description
='Naming Components for displacement maps')
716 transmission
: StringProperty(
718 default
='transmission transparency',
719 description
='Naming Components for transmission maps')
720 emission
: StringProperty(
722 default
='emission emissive emit',
723 description
='Naming Components for emission maps')
724 alpha
: StringProperty(
726 default
='alpha opacity',
727 description
='Naming Components for alpha maps')
728 ambient_occlusion
: StringProperty(
729 name
='Ambient Occlusion',
730 default
='ao ambient occlusion',
731 description
='Naming Components for AO maps')
734 class NWNodeWrangler(bpy
.types
.AddonPreferences
):
737 merge_hide
: EnumProperty(
738 name
="Hide Mix nodes",
740 ("ALWAYS", "Always", "Always collapse the new merge nodes"),
741 ("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
742 ("NEVER", "Never", "Never collapse the new merge nodes")
744 default
='NON_SHADER',
745 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
746 merge_position
: EnumProperty(
747 name
="Mix Node Position",
749 ("CENTER", "Center", "Place the Mix node between the two nodes"),
750 ("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
753 description
="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
755 show_hotkey_list
: BoolProperty(
756 name
="Show Hotkey List",
758 description
="Expand this box into a list of all the hotkeys for functions in this addon"
760 hotkey_list_filter
: StringProperty(
761 name
=" Filter by Name",
763 description
="Show only hotkeys that have this text in their name",
764 options
={'TEXTEDIT_UPDATE'}
766 show_principled_lists
: BoolProperty(
767 name
="Show Principled naming tags",
769 description
="Expand this box into a list of all naming tags for principled texture setup"
771 principled_tags
: bpy
.props
.PointerProperty(type=NWPrincipledPreferences
)
773 def draw(self
, context
):
775 col
= layout
.column()
776 col
.prop(self
, "merge_position")
777 col
.prop(self
, "merge_hide")
780 col
= box
.column(align
=True)
781 col
.prop(self
, "show_principled_lists", text
='Edit tags for auto texture detection in Principled BSDF setup', toggle
=True)
782 if self
.show_principled_lists
:
783 tags
= self
.principled_tags
785 col
.prop(tags
, "base_color")
786 col
.prop(tags
, "sss_color")
787 col
.prop(tags
, "metallic")
788 col
.prop(tags
, "specular")
789 col
.prop(tags
, "rough")
790 col
.prop(tags
, "gloss")
791 col
.prop(tags
, "normal")
792 col
.prop(tags
, "bump")
793 col
.prop(tags
, "displacement")
794 col
.prop(tags
, "transmission")
795 col
.prop(tags
, "emission")
796 col
.prop(tags
, "alpha")
797 col
.prop(tags
, "ambient_occlusion")
800 col
= box
.column(align
=True)
801 hotkey_button_name
= "Show Hotkey List"
802 if self
.show_hotkey_list
:
803 hotkey_button_name
= "Hide Hotkey List"
804 col
.prop(self
, "show_hotkey_list", text
=hotkey_button_name
, toggle
=True)
805 if self
.show_hotkey_list
:
806 col
.prop(self
, "hotkey_list_filter", icon
="VIEWZOOM")
808 for hotkey
in kmi_defs
:
810 hotkey_name
= hotkey
[7]
812 if self
.hotkey_list_filter
.lower() in hotkey_name
.lower():
813 row
= col
.row(align
=True)
814 row
.label(text
=hotkey_name
)
815 keystr
= nice_hotkey_name(hotkey
[1])
817 keystr
= "Shift " + keystr
819 keystr
= "Alt " + keystr
821 keystr
= "Ctrl " + keystr
822 row
.label(text
=keystr
)
826 def nw_check(context
):
827 space
= context
.space_data
828 valid_trees
= ["ShaderNodeTree", "CompositorNodeTree", "TextureNodeTree", "GeometryNodeTree"]
830 if (space
.type == 'NODE_EDITOR'
831 and space
.node_tree
is not None
832 and space
.node_tree
.library
is None
833 and space
.tree_type
in valid_trees
):
840 def poll(cls
, context
):
841 return nw_check(context
)
845 class NWLazyMix(Operator
, NWBase
):
846 """Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
847 bl_idname
= "node.nw_lazy_mix"
848 bl_label
= "Mix Nodes"
849 bl_options
= {'REGISTER', 'UNDO'}
851 def modal(self
, context
, event
):
852 context
.area
.tag_redraw()
853 nodes
, links
= get_nodes_links(context
)
856 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
859 if not context
.scene
.NWBusyDrawing
:
860 node1
= node_at_pos(nodes
, context
, event
)
862 context
.scene
.NWBusyDrawing
= node1
.name
864 if context
.scene
.NWBusyDrawing
!= 'STOP':
865 node1
= nodes
[context
.scene
.NWBusyDrawing
]
867 context
.scene
.NWLazySource
= node1
.name
868 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
870 if event
.type == 'MOUSEMOVE':
871 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
873 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
874 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
875 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
878 node2
= node_at_pos(nodes
, context
, event
)
880 context
.scene
.NWBusyDrawing
= node2
.name
892 bpy
.ops
.node
.nw_merge_nodes(mode
="MIX", merge_type
="AUTO")
894 context
.scene
.NWBusyDrawing
= ""
897 elif event
.type == 'ESC':
899 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
902 return {'RUNNING_MODAL'}
904 def invoke(self
, context
, event
):
905 if context
.area
.type == 'NODE_EDITOR':
906 # the arguments we pass the the callback
907 args
= (self
, context
, 'MIX')
908 # Add the region OpenGL drawing callback
909 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
910 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
914 context
.window_manager
.modal_handler_add(self
)
915 return {'RUNNING_MODAL'}
917 self
.report({'WARNING'}, "View3D not found, cannot run operator")
921 class NWLazyConnect(Operator
, NWBase
):
922 """Connect two nodes without clicking a specific socket (automatically determined"""
923 bl_idname
= "node.nw_lazy_connect"
924 bl_label
= "Lazy Connect"
925 bl_options
= {'REGISTER', 'UNDO'}
926 with_menu
: BoolProperty()
928 def modal(self
, context
, event
):
929 context
.area
.tag_redraw()
930 nodes
, links
= get_nodes_links(context
)
933 start_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
936 if not context
.scene
.NWBusyDrawing
:
937 node1
= node_at_pos(nodes
, context
, event
)
939 context
.scene
.NWBusyDrawing
= node1
.name
941 if context
.scene
.NWBusyDrawing
!= 'STOP':
942 node1
= nodes
[context
.scene
.NWBusyDrawing
]
944 context
.scene
.NWLazySource
= node1
.name
945 context
.scene
.NWLazyTarget
= node_at_pos(nodes
, context
, event
).name
947 if event
.type == 'MOUSEMOVE':
948 self
.mouse_path
.append((event
.mouse_region_x
, event
.mouse_region_y
))
950 elif event
.type == 'RIGHTMOUSE' and event
.value
== 'RELEASE':
951 end_pos
= [event
.mouse_region_x
, event
.mouse_region_y
]
952 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
955 node2
= node_at_pos(nodes
, context
, event
)
957 context
.scene
.NWBusyDrawing
= node2
.name
968 if node
.select
== True:
970 original_sel
.append(node
)
972 original_unsel
.append(node
)
976 #link_success = autolink(node1, node2, links)
978 if len(node1
.outputs
) > 1 and node2
.inputs
:
979 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListOutputs
.bl_idname
)
980 elif len(node1
.outputs
) == 1:
981 bpy
.ops
.node
.nw_call_inputs_menu(from_socket
=0)
983 link_success
= autolink(node1
, node2
, links
)
985 for node
in original_sel
:
987 for node
in original_unsel
:
991 force_update(context
)
992 context
.scene
.NWBusyDrawing
= ""
995 elif event
.type == 'ESC':
996 bpy
.types
.SpaceNodeEditor
.draw_handler_remove(self
._handle
, 'WINDOW')
999 return {'RUNNING_MODAL'}
1001 def invoke(self
, context
, event
):
1002 if context
.area
.type == 'NODE_EDITOR':
1003 nodes
, links
= get_nodes_links(context
)
1004 node
= node_at_pos(nodes
, context
, event
)
1006 context
.scene
.NWBusyDrawing
= node
.name
1008 # the arguments we pass the the callback
1012 args
= (self
, context
, mode
)
1013 # Add the region OpenGL drawing callback
1014 # draw in view space with 'POST_VIEW' and 'PRE_VIEW'
1015 self
._handle
= bpy
.types
.SpaceNodeEditor
.draw_handler_add(draw_callback_nodeoutline
, args
, 'WINDOW', 'POST_PIXEL')
1017 self
.mouse_path
= []
1019 context
.window_manager
.modal_handler_add(self
)
1020 return {'RUNNING_MODAL'}
1022 self
.report({'WARNING'}, "View3D not found, cannot run operator")
1023 return {'CANCELLED'}
1026 class NWDeleteUnused(Operator
, NWBase
):
1027 """Delete all nodes whose output is not used"""
1028 bl_idname
= 'node.nw_del_unused'
1029 bl_label
= 'Delete Unused Nodes'
1030 bl_options
= {'REGISTER', 'UNDO'}
1032 delete_muted
: BoolProperty(name
="Delete Muted", description
="Delete (but reconnect, like Ctrl-X) all muted nodes", default
=True)
1033 delete_frames
: BoolProperty(name
="Delete Empty Frames", description
="Delete all frames that have no nodes inside them", default
=True)
1035 def is_unused_node(self
, node
):
1036 end_types
= ['OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
1037 'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LIGHT', \
1038 'OUTPUT_WORLD', 'GROUP_INPUT', 'GROUP_OUTPUT', 'FRAME']
1039 if node
.type in end_types
:
1042 for output
in node
.outputs
:
1048 def poll(cls
, context
):
1050 if nw_check(context
):
1051 if context
.space_data
.node_tree
.nodes
:
1055 def execute(self
, context
):
1056 nodes
, links
= get_nodes_links(context
)
1061 if node
.select
== True:
1062 selection
.append(node
.name
)
1068 temp_deleted_nodes
= []
1069 del_unused_iterations
= len(nodes
)
1070 for it
in range(0, del_unused_iterations
):
1071 temp_deleted_nodes
= list(deleted_nodes
) # keep record of last iteration
1073 if self
.is_unused_node(node
):
1075 deleted_nodes
.append(node
.name
)
1076 bpy
.ops
.node
.delete()
1078 if temp_deleted_nodes
== deleted_nodes
: # stop iterations when there are no more nodes to be deleted
1081 if self
.delete_frames
:
1089 frames_in_use
.append(node
.parent
)
1091 if node
.type == 'FRAME' and node
not in frames_in_use
:
1094 repeat
= True # repeat for nested frames
1096 if node
not in frames_in_use
:
1098 deleted_nodes
.append(node
.name
)
1099 bpy
.ops
.node
.delete()
1101 if self
.delete_muted
:
1105 deleted_nodes
.append(node
.name
)
1106 bpy
.ops
.node
.delete_reconnect()
1108 # get unique list of deleted nodes (iterations would count the same node more than once)
1109 deleted_nodes
= list(set(deleted_nodes
))
1110 for n
in deleted_nodes
:
1111 self
.report({'INFO'}, "Node " + n
+ " deleted")
1112 num_deleted
= len(deleted_nodes
)
1117 self
.report({'INFO'}, "Deleted " + str(num_deleted
) + n
)
1119 self
.report({'INFO'}, "Nothing deleted")
1122 nodes
, links
= get_nodes_links(context
)
1124 if node
.name
in selection
:
1128 def invoke(self
, context
, event
):
1129 return context
.window_manager
.invoke_confirm(self
, event
)
1132 class NWSwapLinks(Operator
, NWBase
):
1133 """Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
1134 bl_idname
= 'node.nw_swap_links'
1135 bl_label
= 'Swap Links'
1136 bl_options
= {'REGISTER', 'UNDO'}
1139 def poll(cls
, context
):
1141 if nw_check(context
):
1142 if context
.selected_nodes
:
1143 valid
= len(context
.selected_nodes
) <= 2
1146 def execute(self
, context
):
1147 nodes
, links
= get_nodes_links(context
)
1148 selected_nodes
= context
.selected_nodes
1149 n1
= selected_nodes
[0]
1152 if len(selected_nodes
) == 2:
1153 n2
= selected_nodes
[1]
1154 if n1
.outputs
and n2
.outputs
:
1159 for output
in n1
.outputs
:
1161 for link
in output
.links
:
1162 n1_outputs
.append([out_index
, link
.to_socket
])
1167 for output
in n2
.outputs
:
1169 for link
in output
.links
:
1170 n2_outputs
.append([out_index
, link
.to_socket
])
1174 for connection
in n1_outputs
:
1176 links
.new(n2
.outputs
[connection
[0]], connection
[1])
1178 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1179 for connection
in n2_outputs
:
1181 links
.new(n1
.outputs
[connection
[0]], connection
[1])
1183 self
.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
1185 if n1
.outputs
or n2
.outputs
:
1186 self
.report({'WARNING'}, "One of the nodes has no outputs!")
1188 self
.report({'WARNING'}, "Neither of the nodes have outputs!")
1191 elif len(selected_nodes
) == 1:
1192 if n1
.inputs
and n1
.inputs
[0].is_multi_input
:
1193 self
.report({'WARNING'}, "Can't swap inputs of a multi input socket!")
1198 for i1
in n1
.inputs
:
1199 if i1
.is_linked
and not i1
.is_multi_input
:
1201 for i2
in n1
.inputs
:
1202 if i1
.type == i2
.type and i2
.is_linked
and not i2
.is_multi_input
:
1204 types
.append ([i1
, similar_types
, i
])
1206 types
.sort(key
=lambda k
: k
[1], reverse
=True)
1211 for i2
in n1
.inputs
:
1212 if t
[0].type == i2
.type == t
[0].type and t
[0] != i2
and i2
.is_linked
:
1214 i1f
= pair
[0].links
[0].from_socket
1215 i1t
= pair
[0].links
[0].to_socket
1216 i2f
= pair
[1].links
[0].from_socket
1217 i2t
= pair
[1].links
[0].to_socket
1222 fs
= t
[0].links
[0].from_socket
1224 links
.remove(t
[0].links
[0])
1225 if i
+1 == len(n1
.inputs
):
1228 while n1
.inputs
[i
].is_linked
:
1230 links
.new(fs
, n1
.inputs
[i
])
1231 elif len(types
) == 2:
1232 i1f
= types
[0][0].links
[0].from_socket
1233 i1t
= types
[0][0].links
[0].to_socket
1234 i2f
= types
[1][0].links
[0].from_socket
1235 i2t
= types
[1][0].links
[0].to_socket
1240 self
.report({'WARNING'}, "This node has no input connections to swap!")
1242 self
.report({'WARNING'}, "This node has no inputs to swap!")
1244 force_update(context
)
1248 class NWResetBG(Operator
, NWBase
):
1249 """Reset the zoom and position of the background image"""
1250 bl_idname
= 'node.nw_bg_reset'
1251 bl_label
= 'Reset Backdrop'
1252 bl_options
= {'REGISTER', 'UNDO'}
1255 def poll(cls
, context
):
1257 if nw_check(context
):
1258 snode
= context
.space_data
1259 valid
= snode
.tree_type
== 'CompositorNodeTree'
1262 def execute(self
, context
):
1263 context
.space_data
.backdrop_zoom
= 1
1264 context
.space_data
.backdrop_offset
[0] = 0
1265 context
.space_data
.backdrop_offset
[1] = 0
1269 class NWAddAttrNode(Operator
, NWBase
):
1270 """Add an Attribute node with this name"""
1271 bl_idname
= 'node.nw_add_attr_node'
1272 bl_label
= 'Add UV map'
1273 bl_options
= {'REGISTER', 'UNDO'}
1275 attr_name
: StringProperty()
1277 def execute(self
, context
):
1278 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type="ShaderNodeAttribute")
1279 nodes
, links
= get_nodes_links(context
)
1280 nodes
.active
.attribute_name
= self
.attr_name
1283 class NWPreviewNode(Operator
, NWBase
):
1284 bl_idname
= "node.nw_preview_node"
1285 bl_label
= "Preview Node"
1286 bl_description
= "Connect active node to the Node Group output or the Material Output"
1287 bl_options
= {'REGISTER', 'UNDO'}
1289 # If false, the operator is not executed if the current node group happens to be a geometry nodes group.
1290 # This is needed because geometry nodes has its own viewer node that uses the same shortcut as in the compositor.
1291 # Geometry Nodes support can be removed here once the viewer node is supported in the viewport.
1292 run_in_geometry_nodes
: BoolProperty(default
=True)
1295 self
.shader_output_type
= ""
1296 self
.shader_output_ident
= ""
1299 def poll(cls
, context
):
1300 if nw_check(context
):
1301 space
= context
.space_data
1302 if space
.tree_type
== 'ShaderNodeTree' or space
.tree_type
== 'GeometryNodeTree':
1303 if context
.active_node
:
1304 if context
.active_node
.type != "OUTPUT_MATERIAL" or context
.active_node
.type != "OUTPUT_WORLD":
1310 def ensure_viewer_socket(self
, node
, socket_type
, connect_socket
=None):
1311 #check if a viewer output already exists in a node group otherwise create
1312 if hasattr(node
, "node_tree"):
1314 if len(node
.node_tree
.outputs
):
1316 for i
, socket
in enumerate(node
.node_tree
.outputs
):
1317 if is_viewer_socket(socket
) and is_visible_socket(node
.outputs
[i
]) and socket
.type == socket_type
:
1318 #if viewer output is already used but leads to the same socket we can still use it
1319 is_used
= self
.is_socket_used_other_mats(socket
)
1321 if connect_socket
== None:
1323 groupout
= get_group_output_node(node
.node_tree
)
1324 groupout_input
= groupout
.inputs
[i
]
1325 links
= groupout_input
.links
1326 if connect_socket
not in [link
.from_socket
for link
in links
]:
1332 if not index
and free_socket
:
1336 #create viewer socket
1337 node
.node_tree
.outputs
.new(socket_type
, viewer_socket_name
)
1338 index
= len(node
.node_tree
.outputs
) - 1
1339 node
.node_tree
.outputs
[index
].NWViewerSocket
= True
1342 def init_shader_variables(self
, space
, shader_type
):
1343 if shader_type
== 'OBJECT':
1344 if space
.id not in [light
for light
in bpy
.data
.lights
]: # cannot use bpy.data.lights directly as iterable
1345 self
.shader_output_type
= "OUTPUT_MATERIAL"
1346 self
.shader_output_ident
= "ShaderNodeOutputMaterial"
1348 self
.shader_output_type
= "OUTPUT_LIGHT"
1349 self
.shader_output_ident
= "ShaderNodeOutputLight"
1351 elif shader_type
== 'WORLD':
1352 self
.shader_output_type
= "OUTPUT_WORLD"
1353 self
.shader_output_ident
= "ShaderNodeOutputWorld"
1355 def get_shader_output_node(self
, tree
):
1356 for node
in tree
.nodes
:
1357 if node
.type == self
.shader_output_type
and node
.is_active_output
== True:
1361 def ensure_group_output(cls
, tree
):
1362 #check if a group output node exists otherwise create
1363 groupout
= get_group_output_node(tree
)
1365 groupout
= tree
.nodes
.new('NodeGroupOutput')
1366 loc_x
, loc_y
= get_output_location(tree
)
1367 groupout
.location
.x
= loc_x
1368 groupout
.location
.y
= loc_y
1369 groupout
.select
= False
1370 # So that we don't keep on adding new group outputs
1371 groupout
.is_active_output
= True
1375 def search_sockets(cls
, node
, sockets
, index
=None):
1376 # recursively scan nodes for viewer sockets and store in list
1377 for i
, input_socket
in enumerate(node
.inputs
):
1378 if index
and i
!= index
:
1380 if len(input_socket
.links
):
1381 link
= input_socket
.links
[0]
1382 next_node
= link
.from_node
1383 external_socket
= link
.from_socket
1384 if hasattr(next_node
, "node_tree"):
1385 for socket_index
, s
in enumerate(next_node
.outputs
):
1386 if s
== external_socket
:
1388 socket
= next_node
.node_tree
.outputs
[socket_index
]
1389 if is_viewer_socket(socket
) and socket
not in sockets
:
1390 sockets
.append(socket
)
1391 #continue search inside of node group but restrict socket to where we came from
1392 groupout
= get_group_output_node(next_node
.node_tree
)
1393 cls
.search_sockets(groupout
, sockets
, index
=socket_index
)
1396 def scan_nodes(cls
, tree
, sockets
):
1397 # get all viewer sockets in a material tree
1398 for node
in tree
.nodes
:
1399 if hasattr(node
, "node_tree"):
1400 for socket
in node
.node_tree
.outputs
:
1401 if is_viewer_socket(socket
) and (socket
not in sockets
):
1402 sockets
.append(socket
)
1403 cls
.scan_nodes(node
.node_tree
, sockets
)
1405 def link_leads_to_used_socket(self
, link
):
1406 #return True if link leads to a socket that is already used in this material
1407 socket
= get_internal_socket(link
.to_socket
)
1408 return (socket
and self
.is_socket_used_active_mat(socket
))
1410 def is_socket_used_active_mat(self
, socket
):
1411 #ensure used sockets in active material is calculated and check given socket
1412 if not hasattr(self
, "used_viewer_sockets_active_mat"):
1413 self
.used_viewer_sockets_active_mat
= []
1414 materialout
= self
.get_shader_output_node(bpy
.context
.space_data
.node_tree
)
1416 self
.search_sockets(materialout
, self
.used_viewer_sockets_active_mat
)
1417 return socket
in self
.used_viewer_sockets_active_mat
1419 def is_socket_used_other_mats(self
, socket
):
1420 #ensure used sockets in other materials are calculated and check given socket
1421 if not hasattr(self
, "used_viewer_sockets_other_mats"):
1422 self
.used_viewer_sockets_other_mats
= []
1423 for mat
in bpy
.data
.materials
:
1424 if mat
.node_tree
== bpy
.context
.space_data
.node_tree
or not hasattr(mat
.node_tree
, "nodes"):
1427 materialout
= self
.get_shader_output_node(mat
.node_tree
)
1429 self
.search_sockets(materialout
, self
.used_viewer_sockets_other_mats
)
1430 return socket
in self
.used_viewer_sockets_other_mats
1432 def invoke(self
, context
, event
):
1433 space
= context
.space_data
1434 # Ignore operator when running in wrong context.
1435 if self
.run_in_geometry_nodes
!= (space
.tree_type
== "GeometryNodeTree"):
1436 return {'PASS_THROUGH'}
1438 shader_type
= space
.shader_type
1439 self
.init_shader_variables(space
, shader_type
)
1440 mlocx
= event
.mouse_region_x
1441 mlocy
= event
.mouse_region_y
1442 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
1443 if 'FINISHED' in select_node
: # only run if mouse click is on a node
1444 active_tree
, path_to_tree
= get_active_tree(context
)
1445 nodes
, links
= active_tree
.nodes
, active_tree
.links
1446 base_node_tree
= space
.node_tree
1447 active
= nodes
.active
1449 # For geometry node trees we just connect to the group output
1450 if space
.tree_type
== "GeometryNodeTree":
1453 for out
in active
.outputs
:
1454 if is_visible_socket(out
):
1463 # Scan through all nodes in tree including nodes inside of groups to find viewer sockets
1464 self
.scan_nodes(base_node_tree
, delete_sockets
)
1466 # Find (or create if needed) the output of this node tree
1467 geometryoutput
= self
.ensure_group_output(base_node_tree
)
1469 # Analyze outputs, make links
1472 for i
, out
in enumerate(active
.outputs
):
1473 if is_visible_socket(out
) and out
.type == 'GEOMETRY':
1474 valid_outputs
.append(i
)
1476 out_i
= valid_outputs
[0] # Start index of node's outputs
1477 for i
, valid_i
in enumerate(valid_outputs
):
1478 for out_link
in active
.outputs
[valid_i
].links
:
1479 if is_viewer_link(out_link
, geometryoutput
):
1480 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1481 if i
< len(valid_outputs
) - 1:
1482 out_i
= valid_outputs
[i
+ 1]
1484 out_i
= valid_outputs
[0]
1486 make_links
= [] # store sockets for new links
1488 # If there is no 'GEOMETRY' output type - We can't preview the node
1491 socket_type
= 'GEOMETRY'
1492 # Find an input socket of the output of type geometry
1493 geometryoutindex
= None
1494 for i
,inp
in enumerate(geometryoutput
.inputs
):
1495 if inp
.type == socket_type
:
1496 geometryoutindex
= i
1498 if geometryoutindex
is None:
1499 # Create geometry socket
1500 geometryoutput
.inputs
.new(socket_type
, 'Geometry')
1501 geometryoutindex
= len(geometryoutput
.inputs
) - 1
1503 make_links
.append((active
.outputs
[out_i
], geometryoutput
.inputs
[geometryoutindex
]))
1504 output_socket
= geometryoutput
.inputs
[geometryoutindex
]
1505 for li_from
, li_to
in make_links
:
1506 base_node_tree
.links
.new(li_from
, li_to
)
1507 tree
= base_node_tree
1508 link_end
= output_socket
1509 while tree
.nodes
.active
!= active
:
1510 node
= tree
.nodes
.active
1511 index
= self
.ensure_viewer_socket(node
,'NodeSocketGeometry', connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1512 link_start
= node
.outputs
[index
]
1513 node_socket
= node
.node_tree
.outputs
[index
]
1514 if node_socket
in delete_sockets
:
1515 delete_sockets
.remove(node_socket
)
1516 tree
.links
.new(link_start
, link_end
)
1518 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1519 tree
= tree
.nodes
.active
.node_tree
1520 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1523 for socket
in delete_sockets
:
1524 tree
= socket
.id_data
1525 tree
.outputs
.remove(socket
)
1527 nodes
.active
= active
1528 active
.select
= True
1529 force_update(context
)
1533 # What follows is code for the shader editor
1534 output_types
= [x
.nodetype
for x
in
1535 get_nodes_from_category('Output', context
)]
1538 if active
.rna_type
.identifier
not in output_types
:
1539 for out
in active
.outputs
:
1540 if is_visible_socket(out
):
1544 # get material_output node
1545 materialout
= None # placeholder node
1548 #scan through all nodes in tree including nodes inside of groups to find viewer sockets
1549 self
.scan_nodes(base_node_tree
, delete_sockets
)
1551 materialout
= self
.get_shader_output_node(base_node_tree
)
1553 materialout
= base_node_tree
.nodes
.new(self
.shader_output_ident
)
1554 materialout
.location
= get_output_location(base_node_tree
)
1555 materialout
.select
= False
1559 for i
, out
in enumerate(active
.outputs
):
1560 if is_visible_socket(out
):
1561 valid_outputs
.append(i
)
1563 out_i
= valid_outputs
[0] # Start index of node's outputs
1564 for i
, valid_i
in enumerate(valid_outputs
):
1565 for out_link
in active
.outputs
[valid_i
].links
:
1566 if is_viewer_link(out_link
, materialout
):
1567 if nodes
== base_node_tree
.nodes
or self
.link_leads_to_used_socket(out_link
):
1568 if i
< len(valid_outputs
) - 1:
1569 out_i
= valid_outputs
[i
+ 1]
1571 out_i
= valid_outputs
[0]
1573 make_links
= [] # store sockets for new links
1575 socket_type
= 'NodeSocketShader'
1576 materialout_index
= 1 if active
.outputs
[out_i
].name
== "Volume" else 0
1577 make_links
.append((active
.outputs
[out_i
], materialout
.inputs
[materialout_index
]))
1578 output_socket
= materialout
.inputs
[materialout_index
]
1579 for li_from
, li_to
in make_links
:
1580 base_node_tree
.links
.new(li_from
, li_to
)
1582 # Create links through node groups until we reach the active node
1583 tree
= base_node_tree
1584 link_end
= output_socket
1585 while tree
.nodes
.active
!= active
:
1586 node
= tree
.nodes
.active
1587 index
= self
.ensure_viewer_socket(node
, socket_type
, connect_socket
=active
.outputs
[out_i
] if node
.node_tree
.nodes
.active
== active
else None)
1588 link_start
= node
.outputs
[index
]
1589 node_socket
= node
.node_tree
.outputs
[index
]
1590 if node_socket
in delete_sockets
:
1591 delete_sockets
.remove(node_socket
)
1592 tree
.links
.new(link_start
, link_end
)
1594 link_end
= self
.ensure_group_output(node
.node_tree
).inputs
[index
]
1595 tree
= tree
.nodes
.active
.node_tree
1596 tree
.links
.new(active
.outputs
[out_i
], link_end
)
1599 for socket
in delete_sockets
:
1600 if not self
.is_socket_used_other_mats(socket
):
1601 tree
= socket
.id_data
1602 tree
.outputs
.remove(socket
)
1604 nodes
.active
= active
1605 active
.select
= True
1607 force_update(context
)
1611 return {'CANCELLED'}
1614 class NWFrameSelected(Operator
, NWBase
):
1615 bl_idname
= "node.nw_frame_selected"
1616 bl_label
= "Frame Selected"
1617 bl_description
= "Add a frame node and parent the selected nodes to it"
1618 bl_options
= {'REGISTER', 'UNDO'}
1620 label_prop
: StringProperty(
1622 description
='The visual name of the frame node',
1625 use_custom_color_prop
: BoolProperty(
1626 name
="Custom Color",
1627 description
="Use custom color for the frame node",
1630 color_prop
: FloatVectorProperty(
1632 description
="The color of the frame node",
1633 default
=(0.604, 0.604, 0.604),
1634 min=0, max=1, step
=1, precision
=3,
1635 subtype
='COLOR_GAMMA', size
=3
1638 def draw(self
, context
):
1639 layout
= self
.layout
1640 layout
.prop(self
, 'label_prop')
1641 layout
.prop(self
, 'use_custom_color_prop')
1642 col
= layout
.column()
1643 col
.active
= self
.use_custom_color_prop
1644 col
.prop(self
, 'color_prop', text
="")
1646 def execute(self
, context
):
1647 nodes
, links
= get_nodes_links(context
)
1650 if node
.select
== True:
1651 selected
.append(node
)
1653 bpy
.ops
.node
.add_node(type='NodeFrame')
1655 frm
.label
= self
.label_prop
1656 frm
.use_custom_color
= self
.use_custom_color_prop
1657 frm
.color
= self
.color_prop
1659 for node
in selected
:
1665 class NWReloadImages(Operator
):
1666 bl_idname
= "node.nw_reload_images"
1667 bl_label
= "Reload Images"
1668 bl_description
= "Update all the image nodes to match their files on disk"
1671 def poll(cls
, context
):
1673 if nw_check(context
) and context
.space_data
.tree_type
!= 'GeometryNodeTree':
1674 if context
.active_node
is not None:
1675 for out
in context
.active_node
.outputs
:
1676 if is_visible_socket(out
):
1681 def execute(self
, context
):
1682 nodes
, links
= get_nodes_links(context
)
1683 image_types
= ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
1686 if node
.type in image_types
:
1687 if node
.type == "TEXTURE":
1688 if node
.texture
: # node has texture assigned
1689 if node
.texture
.type in ['IMAGE', 'ENVIRONMENT_MAP']:
1690 if node
.texture
.image
: # texture has image assigned
1691 node
.texture
.image
.reload()
1699 self
.report({'INFO'}, "Reloaded images")
1700 print("Reloaded " + str(num_reloaded
) + " images")
1701 force_update(context
)
1704 self
.report({'WARNING'}, "No images found to reload in this node tree")
1705 return {'CANCELLED'}
1708 class NWSwitchNodeType(Operator
, NWBase
):
1709 """Switch type of selected nodes """
1710 bl_idname
= "node.nw_swtch_node_type"
1711 bl_label
= "Switch Node Type"
1712 bl_options
= {'REGISTER', 'UNDO'}
1714 to_type
: StringProperty(
1715 name
="Switch to type",
1719 def execute(self
, context
):
1720 to_type
= self
.to_type
1721 if len(to_type
) == 0:
1722 return {'CANCELLED'}
1724 nodes
, links
= get_nodes_links(context
)
1725 # Those types of nodes will not swap.
1726 src_excludes
= ('NodeFrame')
1727 # Those attributes of nodes will be copied if possible
1728 attrs_to_pass
= ('color', 'hide', 'label', 'mute', 'parent',
1729 'show_options', 'show_preview', 'show_texture',
1730 'use_alpha', 'use_clamp', 'use_custom_color', 'location'
1732 selected
= [n
for n
in nodes
if n
.select
]
1734 for node
in [n
for n
in selected
if
1735 n
.rna_type
.identifier
not in src_excludes
and
1736 n
.rna_type
.identifier
!= to_type
]:
1737 new_node
= nodes
.new(to_type
)
1738 for attr
in attrs_to_pass
:
1739 if hasattr(node
, attr
) and hasattr(new_node
, attr
):
1740 setattr(new_node
, attr
, getattr(node
, attr
))
1741 # set image datablock of dst to image of src
1742 if hasattr(node
, 'image') and hasattr(new_node
, 'image'):
1744 new_node
.image
= node
.image
1746 if new_node
.type == 'SWITCH':
1747 new_node
.hide
= True
1748 # Dictionaries: src_sockets and dst_sockets:
1749 # 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
1750 # 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
1751 # in 'INPUTS' and 'OUTPUTS':
1752 # 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
1754 # (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1756 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1757 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1760 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1761 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
1763 types_order_one
= 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
1764 types_order_two
= 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
1765 # check src node to set src_sockets values and dst node to set dst_sockets dict values
1766 for sockets
, nd
in ((src_sockets
, node
), (dst_sockets
, new_node
)):
1767 # Check node's inputs and outputs and fill proper entries in "sockets" dict
1768 for in_out
, in_out_name
in ((nd
.inputs
, 'INPUTS'), (nd
.outputs
, 'OUTPUTS')):
1769 # enumerate in inputs, then in outputs
1770 # find name, default value and links of socket
1771 for i
, socket
in enumerate(in_out
):
1772 the_name
= socket
.name
1774 # Not every socket, especially in outputs has "default_value"
1775 if hasattr(socket
, 'default_value'):
1776 dval
= socket
.default_value
1778 for lnk
in socket
.links
:
1779 socket_links
.append(lnk
)
1780 # check type of socket to fill proper keys.
1781 for the_type
in types_order_one
:
1782 if socket
.type == the_type
:
1783 # create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
1784 # entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
1785 sockets
[in_out_name
][the_type
].append((len(sockets
[in_out_name
][the_type
]), i
, the_name
, dval
, socket_links
))
1786 # Check which of the types in inputs/outputs is considered to be "main".
1787 # Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
1788 for type_check
in types_order_one
:
1789 if sockets
[in_out_name
][type_check
]:
1790 sockets
[in_out_name
]['MAIN'] = type_check
1794 'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1795 'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
1798 for inout
, soctype
in (
1799 ('INPUTS', 'MAIN',),
1800 ('INPUTS', 'SHADER',),
1801 ('INPUTS', 'RGBA',),
1802 ('INPUTS', 'VECTOR',),
1803 ('INPUTS', 'VALUE',),
1804 ('OUTPUTS', 'MAIN',),
1805 ('OUTPUTS', 'SHADER',),
1806 ('OUTPUTS', 'RGBA',),
1807 ('OUTPUTS', 'VECTOR',),
1808 ('OUTPUTS', 'VALUE',),
1810 if src_sockets
[inout
][soctype
] and dst_sockets
[inout
][soctype
]:
1811 if soctype
== 'MAIN':
1812 sc
= src_sockets
[inout
][src_sockets
[inout
]['MAIN']]
1813 dt
= dst_sockets
[inout
][dst_sockets
[inout
]['MAIN']]
1815 sc
= src_sockets
[inout
][soctype
]
1816 dt
= dst_sockets
[inout
][soctype
]
1817 # start with 'dt' to determine number of possibilities.
1818 for i
, soc
in enumerate(dt
):
1819 # if src main has enough entries - match them with dst main sockets by indexes.
1821 matches
[inout
][soctype
].append(((sc
[i
][1], sc
[i
][3]), (soc
[1], soc
[3])))
1822 # add 'VALUE_NAME' criterion to inputs.
1823 if inout
== 'INPUTS' and soctype
== 'VALUE':
1825 if s
[2] == soc
[2]: # if names match
1826 # append src (index, dval), dst (index, dval)
1827 matches
['INPUTS']['VALUE_NAME'].append(((s
[1], s
[3]), (soc
[1], soc
[3])))
1829 # When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
1830 # This creates better links when relinking textures.
1831 if src_sockets
['INPUTS']['MAIN'] == 'VECTOR' and matches
['INPUTS']['VECTOR']:
1832 matches
['INPUTS']['MAIN'] = matches
['INPUTS']['VECTOR']
1834 # Pass default values and RELINK:
1835 for tp
in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
1836 # INPUTS: Base on matches in proper order.
1837 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['INPUTS'][tp
]:
1839 if src_dval
and dst_dval
and tp
in {'RGBA', 'VALUE_NAME'}:
1840 new_node
.inputs
[dst_i
].default_value
= src_dval
1841 # Special case: switch to math
1842 if node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1843 new_node
.type == 'MATH' and\
1845 new_dst_dval
= max(src_dval
[0], src_dval
[1], src_dval
[2])
1846 new_node
.inputs
[dst_i
].default_value
= new_dst_dval
1847 if node
.type == 'MIX_RGB':
1848 if node
.blend_type
in [o
[0] for o
in operations
]:
1849 new_node
.operation
= node
.blend_type
1850 # Special case: switch from math to some types
1851 if node
.type == 'MATH' and\
1852 new_node
.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
1855 new_node
.inputs
[dst_i
].default_value
[i
] = src_dval
1856 if new_node
.type == 'MIX_RGB':
1857 if node
.operation
in [t
[0] for t
in blend_types
]:
1858 new_node
.blend_type
= node
.operation
1859 # Set Fac of MIX_RGB to 1.0
1860 new_node
.inputs
[0].default_value
= 1.0
1861 # make link only when dst matching input is not linked already.
1862 if node
.inputs
[src_i
].links
and not new_node
.inputs
[dst_i
].links
:
1863 in_src_link
= node
.inputs
[src_i
].links
[0]
1864 in_dst_socket
= new_node
.inputs
[dst_i
]
1865 links
.new(in_src_link
.from_socket
, in_dst_socket
)
1866 links
.remove(in_src_link
)
1867 # OUTPUTS: Base on matches in proper order.
1868 for (src_i
, src_dval
), (dst_i
, dst_dval
) in matches
['OUTPUTS'][tp
]:
1869 for out_src_link
in node
.outputs
[src_i
].links
:
1870 out_dst_socket
= new_node
.outputs
[dst_i
]
1871 links
.new(out_dst_socket
, out_src_link
.to_socket
)
1872 # relink rest inputs if possible, no criteria
1873 for src_inp
in node
.inputs
:
1874 for dst_inp
in new_node
.inputs
:
1875 if src_inp
.links
and not dst_inp
.links
:
1876 src_link
= src_inp
.links
[0]
1877 links
.new(src_link
.from_socket
, dst_inp
)
1878 links
.remove(src_link
)
1879 # relink rest outputs if possible, base on node kind if any left.
1880 for src_o
in node
.outputs
:
1881 for out_src_link
in src_o
.links
:
1882 for dst_o
in new_node
.outputs
:
1883 if src_o
.type == dst_o
.type:
1884 links
.new(dst_o
, out_src_link
.to_socket
)
1885 # relink rest outputs no criteria if any left. Link all from first output.
1886 for src_o
in node
.outputs
:
1887 for out_src_link
in src_o
.links
:
1888 if new_node
.outputs
:
1889 links
.new(new_node
.outputs
[0], out_src_link
.to_socket
)
1891 force_update(context
)
1895 class NWMergeNodes(Operator
, NWBase
):
1896 bl_idname
= "node.nw_merge_nodes"
1897 bl_label
= "Merge Nodes"
1898 bl_description
= "Merge Selected Nodes"
1899 bl_options
= {'REGISTER', 'UNDO'}
1903 description
="All possible blend types, boolean operations and math operations",
1904 items
= blend_types
+ [op
for op
in geo_combine_operations
if op
not in blend_types
] + [op
for op
in operations
if op
not in blend_types
],
1906 merge_type
: EnumProperty(
1908 description
="Type of Merge to be used",
1910 ('AUTO', 'Auto', 'Automatic Output Type Detection'),
1911 ('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
1912 ('GEOMETRY', 'Geometry', 'Merge using Boolean or Join Geometry Node'),
1913 ('MIX', 'Mix Node', 'Merge using Mix Nodes'),
1914 ('MATH', 'Math Node', 'Merge using Math Nodes'),
1915 ('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
1916 ('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
1920 # Check if the link connects to a node that is in selected_nodes
1921 # If not, then check recursively for each link in the nodes outputs.
1922 # If yes, return True. If the recursion stops without finding a node
1923 # in selected_nodes, it returns False. The depth is used to prevent
1924 # getting stuck in a loop because of an already present cycle.
1926 def link_creates_cycle(link
, selected_nodes
, depth
=0)->bool:
1928 # We're stuck in a cycle, but that cycle was already present,
1929 # so we return False.
1930 # NOTE: The number 255 is arbitrary, but seems to work well.
1933 if node
in selected_nodes
:
1935 if not node
.outputs
:
1937 for output
in node
.outputs
:
1938 if output
.is_linked
:
1939 for olink
in output
.links
:
1940 if NWMergeNodes
.link_creates_cycle(olink
, selected_nodes
, depth
+1):
1942 # None of the outputs found a node in selected_nodes, so there is no cycle.
1945 # Merge the nodes in `nodes_list` with a node of type `node_name` that has a multi_input socket.
1946 # The parameters `socket_indices` gives the indices of the node sockets in the order that they should
1947 # be connected. The last one is assumed to be a multi input socket.
1948 # For convenience the node is returned.
1950 def merge_with_multi_input(nodes_list
, merge_position
,do_hide
, loc_x
, links
, nodes
, node_name
, socket_indices
):
1951 # The y-location of the last node
1952 loc_y
= nodes_list
[-1][2]
1953 if merge_position
== 'CENTER':
1954 # Average the y-location
1955 for i
in range(len(nodes_list
)-1):
1956 loc_y
+= nodes_list
[i
][2]
1957 loc_y
= loc_y
/len(nodes_list
)
1958 new_node
= nodes
.new(node_name
)
1959 new_node
.hide
= do_hide
1960 new_node
.location
.x
= loc_x
1961 new_node
.location
.y
= loc_y
1962 selected_nodes
= [nodes
[node_info
[0]] for node_info
in nodes_list
]
1964 outputs_for_multi_input
= []
1965 for i
,node
in enumerate(selected_nodes
):
1967 # Search for the first node which had output links that do not create
1968 # a cycle, which we can then reconnect afterwards.
1969 if prev_links
== [] and node
.outputs
[0].is_linked
:
1970 prev_links
= [link
for link
in node
.outputs
[0].links
if not NWMergeNodes
.link_creates_cycle(link
, selected_nodes
)]
1971 # Get the index of the socket, the last one is a multi input, and is thus used repeatedly
1972 # To get the placement to look right we need to reverse the order in which we connect the
1973 # outputs to the multi input socket.
1974 if i
< len(socket_indices
) - 1:
1975 ind
= socket_indices
[i
]
1976 links
.new(node
.outputs
[0], new_node
.inputs
[ind
])
1978 outputs_for_multi_input
.insert(0, node
.outputs
[0])
1979 if outputs_for_multi_input
!= []:
1980 ind
= socket_indices
[-1]
1981 for output
in outputs_for_multi_input
:
1982 links
.new(output
, new_node
.inputs
[ind
])
1983 if prev_links
!= []:
1984 for link
in prev_links
:
1985 links
.new(new_node
.outputs
[0], link
.to_node
.inputs
[0])
1988 def execute(self
, context
):
1989 settings
= context
.preferences
.addons
[__name__
].preferences
1990 merge_hide
= settings
.merge_hide
1991 merge_position
= settings
.merge_position
# 'center' or 'bottom'
1994 do_hide_shader
= False
1995 if merge_hide
== 'ALWAYS':
1997 do_hide_shader
= True
1998 elif merge_hide
== 'NON_SHADER':
2001 tree_type
= context
.space_data
.node_tree
.type
2002 if tree_type
== 'GEOMETRY':
2003 node_type
= 'GeometryNode'
2004 if tree_type
== 'COMPOSITING':
2005 node_type
= 'CompositorNode'
2006 elif tree_type
== 'SHADER':
2007 node_type
= 'ShaderNode'
2008 elif tree_type
== 'TEXTURE':
2009 node_type
= 'TextureNode'
2010 nodes
, links
= get_nodes_links(context
)
2012 merge_type
= self
.merge_type
2013 # Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
2014 # 'ZCOMBINE' works only if mode == 'MIX'
2015 # Setting mode to None prevents trying to add 'ZCOMBINE' node.
2016 if (merge_type
== 'ZCOMBINE' or merge_type
== 'ALPHAOVER') and tree_type
!= 'COMPOSITING':
2019 if (merge_type
!= 'MATH' and merge_type
!= 'GEOMETRY') and tree_type
== 'GEOMETRY':
2021 # The MixRGB node and math nodes used for geometry nodes are of type 'ShaderNode'
2022 if (merge_type
== 'MATH' or merge_type
== 'MIX') and tree_type
== 'GEOMETRY':
2023 node_type
= 'ShaderNode'
2024 selected_mix
= [] # entry = [index, loc]
2025 selected_shader
= [] # entry = [index, loc]
2026 selected_geometry
= [] # entry = [index, loc]
2027 selected_math
= [] # entry = [index, loc]
2028 selected_vector
= [] # entry = [index, loc]
2029 selected_z
= [] # entry = [index, loc]
2030 selected_alphaover
= [] # entry = [index, loc]
2032 for i
, node
in enumerate(nodes
):
2033 if node
.select
and node
.outputs
:
2034 if merge_type
== 'AUTO':
2035 for (type, types_list
, dst
) in (
2036 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2037 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2038 ('RGBA', [t
[0] for t
in blend_types
], selected_mix
),
2039 ('VALUE', [t
[0] for t
in operations
], selected_math
),
2040 ('VECTOR', [], selected_vector
),
2042 output
= get_first_enabled_output(node
)
2043 output_type
= output
.type
2044 valid_mode
= mode
in types_list
2045 # When mode is 'MIX' we have to cheat since the mix node is not used in
2047 if tree_type
== 'GEOMETRY':
2049 if output_type
== 'VALUE' and type == 'VALUE':
2051 elif output_type
== 'VECTOR' and type == 'VECTOR':
2053 elif type == 'GEOMETRY':
2055 # When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
2056 # Cheat that output type is 'RGBA',
2057 # and that 'MIX' exists in math operations list.
2058 # This way when selected_mix list is analyzed:
2059 # Node data will be appended even though it doesn't meet requirements.
2060 elif output_type
!= 'SHADER' and mode
== 'MIX':
2061 output_type
= 'RGBA'
2063 if output_type
== type and valid_mode
:
2064 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2066 for (type, types_list
, dst
) in (
2067 ('SHADER', ('MIX', 'ADD'), selected_shader
),
2068 ('GEOMETRY', [t
[0] for t
in geo_combine_operations
], selected_geometry
),
2069 ('MIX', [t
[0] for t
in blend_types
], selected_mix
),
2070 ('MATH', [t
[0] for t
in operations
], selected_math
),
2071 ('ZCOMBINE', ('MIX', ), selected_z
),
2072 ('ALPHAOVER', ('MIX', ), selected_alphaover
),
2074 if merge_type
== type and mode
in types_list
:
2075 dst
.append([i
, node
.location
.x
, node
.location
.y
, node
.dimensions
.x
, node
.hide
])
2076 # When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
2077 # use only 'Mix' nodes for merging.
2078 # For that we add selected_math list to selected_mix list and clear selected_math.
2079 if selected_mix
and selected_math
and merge_type
== 'AUTO':
2080 selected_mix
+= selected_math
2082 for nodes_list
in [selected_mix
, selected_shader
, selected_geometry
, selected_math
, selected_vector
, selected_z
, selected_alphaover
]:
2085 count_before
= len(nodes
)
2086 # sort list by loc_x - reversed
2087 nodes_list
.sort(key
=lambda k
: k
[1], reverse
=True)
2089 loc_x
= nodes_list
[0][1] + nodes_list
[0][3] + 70
2090 nodes_list
.sort(key
=lambda k
: k
[2], reverse
=True)
2092 # Change the node type for math nodes in a geometry node tree.
2093 if tree_type
== 'GEOMETRY':
2094 if nodes_list
is selected_math
or nodes_list
is selected_vector
or nodes_list
is selected_mix
:
2095 node_type
= 'ShaderNode'
2099 node_type
= 'GeometryNode'
2100 if merge_position
== 'CENTER':
2101 loc_y
= ((nodes_list
[len(nodes_list
) - 1][2]) + (nodes_list
[len(nodes_list
) - 2][2])) / 2 # average yloc of last two nodes (lowest two)
2102 if nodes_list
[len(nodes_list
) - 1][-1] == True: # if last node is hidden, mix should be shifted up a bit
2108 loc_y
= nodes_list
[len(nodes_list
) - 1][2]
2112 if nodes_list
== selected_shader
and not do_hide_shader
:
2114 the_range
= len(nodes_list
) - 1
2115 if len(nodes_list
) == 1:
2118 for i
in range(the_range
):
2119 if nodes_list
== selected_mix
:
2120 add_type
= node_type
+ 'MixRGB'
2121 add
= nodes
.new(add_type
)
2122 add
.blend_type
= mode
2124 add
.inputs
[0].default_value
= 1.0
2125 add
.show_preview
= False
2131 add
.width_hidden
= 100.0
2132 elif nodes_list
== selected_math
:
2133 add_type
= node_type
+ 'Math'
2134 add
= nodes
.new(add_type
)
2135 add
.operation
= mode
2141 add
.width_hidden
= 100.0
2142 elif nodes_list
== selected_shader
:
2144 add_type
= node_type
+ 'MixShader'
2145 add
= nodes
.new(add_type
)
2146 add
.hide
= do_hide_shader
2151 add
.width_hidden
= 100.0
2153 add_type
= node_type
+ 'AddShader'
2154 add
= nodes
.new(add_type
)
2155 add
.hide
= do_hide_shader
2160 add
.width_hidden
= 100.0
2161 elif nodes_list
== selected_geometry
:
2162 if mode
in ('JOIN', 'MIX'):
2163 add_type
= node_type
+ 'JoinGeometry'
2164 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,[0])
2166 add_type
= node_type
+ 'Boolean'
2167 indices
= [0,1] if mode
== 'DIFFERENCE' else [1]
2168 add
= self
.merge_with_multi_input(nodes_list
, merge_position
, do_hide
, loc_x
, links
, nodes
, add_type
,indices
)
2169 add
.operation
= mode
2172 elif nodes_list
== selected_vector
:
2173 add_type
= node_type
+ 'VectorMath'
2174 add
= nodes
.new(add_type
)
2175 add
.operation
= mode
2181 add
.width_hidden
= 100.0
2182 elif nodes_list
== selected_z
:
2183 add
= nodes
.new('CompositorNodeZcombine')
2184 add
.show_preview
= False
2190 add
.width_hidden
= 100.0
2191 elif nodes_list
== selected_alphaover
:
2192 add
= nodes
.new('CompositorNodeAlphaOver')
2193 add
.show_preview
= False
2199 add
.width_hidden
= 100.0
2200 add
.location
= loc_x
, loc_y
2204 # This has already been handled separately
2208 count_after
= len(nodes
)
2209 index
= count_after
- 1
2210 first_selected
= nodes
[nodes_list
[0][0]]
2211 # "last" node has been added as first, so its index is count_before.
2212 last_add
= nodes
[count_before
]
2213 # Create list of invalid indexes.
2214 invalid_nodes
= [nodes
[n
[0]] for n
in (selected_mix
+ selected_math
+ selected_shader
+ selected_z
+ selected_geometry
)]
2217 # Two nodes were selected and first selected has no output links, second selected has output links.
2218 # Then add links from last add to all links 'to_socket' of out links of second selected.
2219 first_selected_output
= get_first_enabled_output(first_selected
)
2220 if len(nodes_list
) == 2:
2221 if not first_selected_output
.links
:
2222 second_selected
= nodes
[nodes_list
[1][0]]
2223 for ss_link
in second_selected
.outputs
[0].links
:
2224 # Prevent cyclic dependencies when nodes to be merged are linked to one another.
2225 # Link only if "to_node" index not in invalid indexes list.
2226 if not self
.link_creates_cycle(ss_link
, invalid_nodes
):
2227 links
.new(last_add
.outputs
[0], ss_link
.to_socket
)
2228 # add links from last_add to all links 'to_socket' of out links of first selected.
2229 for fs_link
in first_selected_output
.links
:
2230 # Link only if "to_node" index not in invalid indexes list.
2231 if not self
.link_creates_cycle(fs_link
, invalid_nodes
):
2232 links
.new(last_add
.outputs
[0], fs_link
.to_socket
)
2233 # add link from "first" selected and "first" add node
2234 node_to
= nodes
[count_after
- 1]
2235 links
.new(first_selected_output
, node_to
.inputs
[first
])
2236 if node_to
.type == 'ZCOMBINE':
2237 for fs_out
in first_selected
.outputs
:
2238 if fs_out
!= first_selected_output
and fs_out
.name
in ('Z', 'Depth'):
2239 links
.new(fs_out
, node_to
.inputs
[1])
2241 # add links between added ADD nodes and between selected and ADD nodes
2242 for i
in range(count_adds
):
2243 if i
< count_adds
- 1:
2244 node_from
= nodes
[index
]
2245 node_to
= nodes
[index
- 1]
2246 node_to_input_i
= first
2247 node_to_z_i
= 1 # if z combine - link z to first z input
2248 links
.new(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
2249 if node_to
.type == 'ZCOMBINE':
2250 for from_out
in node_from
.outputs
:
2251 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
2252 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2253 if len(nodes_list
) > 1:
2254 node_from
= nodes
[nodes_list
[i
+ 1][0]]
2255 node_to
= nodes
[index
]
2256 node_to_input_i
= second
2257 node_to_z_i
= 3 # if z combine - link z to second z input
2258 links
.new(get_first_enabled_output(node_from
), node_to
.inputs
[node_to_input_i
])
2259 if node_to
.type == 'ZCOMBINE':
2260 for from_out
in node_from
.outputs
:
2261 if from_out
!= get_first_enabled_output(node_from
) and from_out
.name
in ('Z', 'Depth'):
2262 links
.new(from_out
, node_to
.inputs
[node_to_z_i
])
2264 # set "last" of added nodes as active
2265 nodes
.active
= last_add
2266 for i
, x
, y
, dx
, h
in nodes_list
:
2267 nodes
[i
].select
= False
2272 class NWBatchChangeNodes(Operator
, NWBase
):
2273 bl_idname
= "node.nw_batch_change"
2274 bl_label
= "Batch Change"
2275 bl_description
= "Batch Change Blend Type and Math Operation"
2276 bl_options
= {'REGISTER', 'UNDO'}
2278 blend_type
: EnumProperty(
2280 items
=blend_types
+ navs
,
2282 operation
: EnumProperty(
2284 items
=operations
+ navs
,
2287 def execute(self
, context
):
2288 blend_type
= self
.blend_type
2289 operation
= self
.operation
2290 for node
in context
.selected_nodes
:
2291 if node
.type == 'MIX_RGB' or node
.bl_idname
== 'GeometryNodeAttributeMix':
2292 if not blend_type
in [nav
[0] for nav
in navs
]:
2293 node
.blend_type
= blend_type
2295 if blend_type
== 'NEXT':
2296 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2297 #index = blend_types.index(node.blend_type)
2298 if index
== len(blend_types
) - 1:
2299 node
.blend_type
= blend_types
[0][0]
2301 node
.blend_type
= blend_types
[index
+ 1][0]
2303 if blend_type
== 'PREV':
2304 index
= [i
for i
, entry
in enumerate(blend_types
) if node
.blend_type
in entry
][0]
2306 node
.blend_type
= blend_types
[len(blend_types
) - 1][0]
2308 node
.blend_type
= blend_types
[index
- 1][0]
2310 if node
.type == 'MATH' or node
.bl_idname
== 'GeometryNodeAttributeMath':
2311 if not operation
in [nav
[0] for nav
in navs
]:
2312 node
.operation
= operation
2314 if operation
== 'NEXT':
2315 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2316 #index = operations.index(node.operation)
2317 if index
== len(operations
) - 1:
2318 node
.operation
= operations
[0][0]
2320 node
.operation
= operations
[index
+ 1][0]
2322 if operation
== 'PREV':
2323 index
= [i
for i
, entry
in enumerate(operations
) if node
.operation
in entry
][0]
2324 #index = operations.index(node.operation)
2326 node
.operation
= operations
[len(operations
) - 1][0]
2328 node
.operation
= operations
[index
- 1][0]
2333 class NWChangeMixFactor(Operator
, NWBase
):
2334 bl_idname
= "node.nw_factor"
2335 bl_label
= "Change Factor"
2336 bl_description
= "Change Factors of Mix Nodes and Mix Shader Nodes"
2337 bl_options
= {'REGISTER', 'UNDO'}
2339 # option: Change factor.
2340 # If option is 1.0 or 0.0 - set to 1.0 or 0.0
2341 # Else - change factor by option value.
2342 option
: FloatProperty()
2344 def execute(self
, context
):
2345 nodes
, links
= get_nodes_links(context
)
2346 option
= self
.option
2347 selected
= [] # entry = index
2348 for si
, node
in enumerate(nodes
):
2350 if node
.type in {'MIX_RGB', 'MIX_SHADER'}:
2354 fac
= nodes
[si
].inputs
[0]
2355 nodes
[si
].hide
= False
2356 if option
in {0.0, 1.0}:
2357 fac
.default_value
= option
2359 fac
.default_value
+= option
2364 class NWCopySettings(Operator
, NWBase
):
2365 bl_idname
= "node.nw_copy_settings"
2366 bl_label
= "Copy Settings"
2367 bl_description
= "Copy Settings of Active Node to Selected Nodes"
2368 bl_options
= {'REGISTER', 'UNDO'}
2371 def poll(cls
, context
):
2373 if nw_check(context
):
2375 context
.active_node
is not None and
2376 context
.active_node
.type != 'FRAME'
2381 def execute(self
, context
):
2382 node_active
= context
.active_node
2383 node_selected
= context
.selected_nodes
2386 if not (len(node_selected
) > 1):
2387 self
.report({'ERROR'}, "2 nodes must be selected at least")
2388 return {'CANCELLED'}
2390 # Check if active node is in the selection
2391 selected_node_names
= [n
.name
for n
in node_selected
]
2392 if node_active
.name
not in selected_node_names
:
2393 self
.report({'ERROR'}, "No active node")
2394 return {'CANCELLED'}
2396 # Get nodes in selection by type
2397 valid_nodes
= [n
for n
in node_selected
if n
.type == node_active
.type]
2399 if not (len(valid_nodes
) > 1) and node_active
:
2400 self
.report({'ERROR'}, "Selected nodes are not of the same type as {}".format(node_active
.name
))
2401 return {'CANCELLED'}
2403 if len(valid_nodes
) != len(node_selected
):
2404 # Report nodes that are not valid
2405 valid_node_names
= [n
.name
for n
in valid_nodes
]
2406 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
2407 self
.report({'INFO'}, "Ignored {} (not of the same type as {})".format(", ".join(not_valid_names
), node_active
.name
))
2409 # Reference original
2411 #node_selected_names = [n.name for n in node_selected]
2416 # Deselect all nodes
2417 for i
in node_selected
:
2420 # Code by zeffii from http://blender.stackexchange.com/a/42338/3710
2421 # Run through all other nodes
2422 for node
in valid_nodes
[1:]:
2424 # Check for frame node
2425 parent
= node
.parent
if node
.parent
else None
2426 node_loc
= [node
.location
.x
, node
.location
.y
]
2428 # Select original to duplicate
2431 # Duplicate selected node
2432 bpy
.ops
.node
.duplicate()
2433 new_node
= context
.selected_nodes
[0]
2436 new_node
.select
= False
2438 # Properties to copy
2439 node_tree
= node
.id_data
2440 props_to_copy
= 'bl_idname name location height width'.split(' ')
2444 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
2445 for i
in (i
for i
in mappings
if i
.is_linked
):
2447 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
2450 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
2451 props_to_copy
.pop(0)
2453 for prop
in props_to_copy
:
2454 setattr(new_node
, prop
, props
[prop
])
2456 # Get the node tree to remove the old node
2457 nodes
= node_tree
.nodes
2459 new_node
.name
= props
['name']
2462 new_node
.parent
= parent
2463 new_node
.location
= node_loc
2465 for str_from
, str_to
in reconnections
:
2466 node_tree
.links
.new(eval(str_from
), eval(str_to
))
2468 success_names
.append(new_node
.name
)
2471 node_tree
.nodes
.active
= orig
2472 self
.report({'INFO'}, "Successfully copied attributes from {} to: {}".format(orig
.name
, ", ".join(success_names
)))
2476 class NWCopyLabel(Operator
, NWBase
):
2477 bl_idname
= "node.nw_copy_label"
2478 bl_label
= "Copy Label"
2479 bl_options
= {'REGISTER', 'UNDO'}
2481 option
: EnumProperty(
2483 description
="Source of name of label",
2485 ('FROM_ACTIVE', 'from active', 'from active node',),
2486 ('FROM_NODE', 'from node', 'from node linked to selected node'),
2487 ('FROM_SOCKET', 'from socket', 'from socket linked to selected node'),
2491 def execute(self
, context
):
2492 nodes
, links
= get_nodes_links(context
)
2493 option
= self
.option
2494 active
= nodes
.active
2495 if option
== 'FROM_ACTIVE':
2497 src_label
= active
.label
2498 for node
in [n
for n
in nodes
if n
.select
and nodes
.active
!= n
]:
2499 node
.label
= src_label
2500 elif option
== 'FROM_NODE':
2501 selected
= [n
for n
in nodes
if n
.select
]
2502 for node
in selected
:
2503 for input in node
.inputs
:
2505 src
= input.links
[0].from_node
2506 node
.label
= src
.label
2508 elif option
== 'FROM_SOCKET':
2509 selected
= [n
for n
in nodes
if n
.select
]
2510 for node
in selected
:
2511 for input in node
.inputs
:
2513 src
= input.links
[0].from_socket
2514 node
.label
= src
.name
2520 class NWClearLabel(Operator
, NWBase
):
2521 bl_idname
= "node.nw_clear_label"
2522 bl_label
= "Clear Label"
2523 bl_options
= {'REGISTER', 'UNDO'}
2525 option
: BoolProperty()
2527 def execute(self
, context
):
2528 nodes
, links
= get_nodes_links(context
)
2529 for node
in [n
for n
in nodes
if n
.select
]:
2534 def invoke(self
, context
, event
):
2536 return self
.execute(context
)
2538 return context
.window_manager
.invoke_confirm(self
, event
)
2541 class NWModifyLabels(Operator
, NWBase
):
2542 """Modify Labels of all selected nodes"""
2543 bl_idname
= "node.nw_modify_labels"
2544 bl_label
= "Modify Labels"
2545 bl_options
= {'REGISTER', 'UNDO'}
2547 prepend
: StringProperty(
2548 name
="Add to Beginning"
2550 append
: StringProperty(
2553 replace_from
: StringProperty(
2554 name
="Text to Replace"
2556 replace_to
: StringProperty(
2560 def execute(self
, context
):
2561 nodes
, links
= get_nodes_links(context
)
2562 for node
in [n
for n
in nodes
if n
.select
]:
2563 node
.label
= self
.prepend
+ node
.label
.replace(self
.replace_from
, self
.replace_to
) + self
.append
2567 def invoke(self
, context
, event
):
2571 return context
.window_manager
.invoke_props_dialog(self
)
2574 class NWAddTextureSetup(Operator
, NWBase
):
2575 bl_idname
= "node.nw_add_texture"
2576 bl_label
= "Texture Setup"
2577 bl_description
= "Add Texture Node Setup to Selected Shaders"
2578 bl_options
= {'REGISTER', 'UNDO'}
2580 add_mapping
: BoolProperty(name
="Add Mapping Nodes", description
="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default
=True)
2583 def poll(cls
, context
):
2584 if nw_check(context
):
2585 space
= context
.space_data
2586 if space
.tree_type
== 'ShaderNodeTree':
2590 def execute(self
, context
):
2591 nodes
, links
= get_nodes_links(context
)
2593 texture_types
= [x
.nodetype
for x
in
2594 get_nodes_from_category('Texture', context
)]
2595 selected_nodes
= [n
for n
in nodes
if n
.select
]
2597 for node
in selected_nodes
:
2602 target_input
= node
.inputs
[0]
2603 for input in node
.inputs
:
2606 if not input.is_linked
:
2607 target_input
= input
2610 self
.report({'WARNING'}, "No free inputs for node: " + node
.name
)
2615 locx
= node
.location
.x
2616 locy
= node
.location
.y
- (input_index
* padding
)
2618 is_texture_node
= node
.rna_type
.identifier
in texture_types
2619 use_environment_texture
= node
.type == 'BACKGROUND'
2621 # Add an image texture before normal shader nodes.
2622 if not is_texture_node
:
2623 image_texture_type
= 'ShaderNodeTexEnvironment' if use_environment_texture
else 'ShaderNodeTexImage'
2624 image_texture_node
= nodes
.new(image_texture_type
)
2625 x_offset
= x_offset
+ image_texture_node
.width
+ padding
2626 image_texture_node
.location
= [locx
- x_offset
, locy
]
2627 nodes
.active
= image_texture_node
2628 links
.new(image_texture_node
.outputs
[0], target_input
)
2630 # The mapping setup following this will connect to the first input of this image texture.
2631 target_input
= image_texture_node
.inputs
[0]
2635 if is_texture_node
or self
.add_mapping
:
2637 mapping_node
= nodes
.new('ShaderNodeMapping')
2638 x_offset
= x_offset
+ mapping_node
.width
+ padding
2639 mapping_node
.location
= [locx
- x_offset
, locy
]
2640 links
.new(mapping_node
.outputs
[0], target_input
)
2642 # Add Texture Coordinates node.
2643 tex_coord_node
= nodes
.new('ShaderNodeTexCoord')
2644 x_offset
= x_offset
+ tex_coord_node
.width
+ padding
2645 tex_coord_node
.location
= [locx
- x_offset
, locy
]
2647 is_procedural_texture
= is_texture_node
and node
.type != 'TEX_IMAGE'
2648 use_generated_coordinates
= is_procedural_texture
or use_environment_texture
2649 tex_coord_output
= tex_coord_node
.outputs
[0 if use_generated_coordinates
else 2]
2650 links
.new(tex_coord_output
, mapping_node
.inputs
[0])
2655 class NWAddPrincipledSetup(Operator
, NWBase
, ImportHelper
):
2656 bl_idname
= "node.nw_add_textures_for_principled"
2657 bl_label
= "Principled Texture Setup"
2658 bl_description
= "Add Texture Node Setup for Principled BSDF"
2659 bl_options
= {'REGISTER', 'UNDO'}
2661 directory
: StringProperty(
2665 description
='Folder to search in for image files'
2667 files
: CollectionProperty(
2668 type=bpy
.types
.OperatorFileListElement
,
2669 options
={'HIDDEN', 'SKIP_SAVE'}
2672 relative_path
: BoolProperty(
2673 name
='Relative Path',
2674 description
='Set the file path relative to the blend file, when possible',
2683 def draw(self
, context
):
2684 layout
= self
.layout
2685 layout
.alignment
= 'LEFT'
2687 layout
.prop(self
, 'relative_path')
2690 def poll(cls
, context
):
2692 if nw_check(context
):
2693 space
= context
.space_data
2694 if space
.tree_type
== 'ShaderNodeTree':
2698 def execute(self
, context
):
2699 # Check if everything is ok
2700 if not self
.directory
:
2701 self
.report({'INFO'}, 'No Folder Selected')
2702 return {'CANCELLED'}
2703 if not self
.files
[:]:
2704 self
.report({'INFO'}, 'No Files Selected')
2705 return {'CANCELLED'}
2707 nodes
, links
= get_nodes_links(context
)
2708 active_node
= nodes
.active
2709 if not (active_node
and active_node
.bl_idname
== 'ShaderNodeBsdfPrincipled'):
2710 self
.report({'INFO'}, 'Select Principled BSDF')
2711 return {'CANCELLED'}
2714 def split_into__components(fname
):
2715 # Split filename into components
2716 # 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
2718 fname
= path
.splitext(fname
)[0]
2720 fname
= ''.join(i
for i
in fname
if not i
.isdigit())
2721 # Separate CamelCase by space
2722 fname
= re
.sub(r
"([a-z])([A-Z])", r
"\g<1> \g<2>",fname
)
2723 # Replace common separators with SPACE
2724 separators
= ['_', '.', '-', '__', '--', '#']
2725 for sep
in separators
:
2726 fname
= fname
.replace(sep
, ' ')
2728 components
= fname
.split(' ')
2729 components
= [c
.lower() for c
in components
]
2732 # Filter textures names for texturetypes in filenames
2733 # [Socket Name, [abbreviations and keyword list], Filename placeholder]
2734 tags
= context
.preferences
.addons
[__name__
].preferences
.principled_tags
2735 normal_abbr
= tags
.normal
.split(' ')
2736 bump_abbr
= tags
.bump
.split(' ')
2737 gloss_abbr
= tags
.gloss
.split(' ')
2738 rough_abbr
= tags
.rough
.split(' ')
2740 ['Displacement', tags
.displacement
.split(' '), None],
2741 ['Base Color', tags
.base_color
.split(' '), None],
2742 ['Subsurface Color', tags
.sss_color
.split(' '), None],
2743 ['Metallic', tags
.metallic
.split(' '), None],
2744 ['Specular', tags
.specular
.split(' '), None],
2745 ['Roughness', rough_abbr
+ gloss_abbr
, None],
2746 ['Normal', normal_abbr
+ bump_abbr
, None],
2747 ['Transmission', tags
.transmission
.split(' '), None],
2748 ['Emission', tags
.emission
.split(' '), None],
2749 ['Alpha', tags
.alpha
.split(' '), None],
2750 ['Ambient Occlusion', tags
.ambient_occlusion
.split(' '), None],
2753 # Look through texture_types and set value as filename of first matched file
2754 def match_files_to_socket_names():
2755 for sname
in socketnames
:
2756 for file in self
.files
:
2758 filenamecomponents
= split_into__components(fname
)
2759 matches
= set(sname
[1]).intersection(set(filenamecomponents
))
2760 # TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
2765 match_files_to_socket_names()
2766 # Remove socketnames without found files
2767 socketnames
= [s
for s
in socketnames
if s
[2]
2768 and path
.exists(self
.directory
+s
[2])]
2770 self
.report({'INFO'}, 'No matching images found')
2771 print('No matching images found')
2772 return {'CANCELLED'}
2774 # Don't override path earlier as os.path is used to check the absolute path
2775 import_path
= self
.directory
2776 if self
.relative_path
:
2777 if bpy
.data
.filepath
:
2779 import_path
= bpy
.path
.relpath(self
.directory
)
2784 print('\nMatched Textures:')
2789 roughness_node
= None
2790 for i
, sname
in enumerate(socketnames
):
2791 print(i
, sname
[0], sname
[2])
2793 # DISPLACEMENT NODES
2794 if sname
[0] == 'Displacement':
2795 disp_texture
= nodes
.new(type='ShaderNodeTexImage')
2796 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2797 disp_texture
.image
= img
2798 disp_texture
.label
= 'Displacement'
2799 if disp_texture
.image
:
2800 disp_texture
.image
.colorspace_settings
.is_data
= True
2802 # Add displacement offset nodes
2803 disp_node
= nodes
.new(type='ShaderNodeDisplacement')
2804 # Align the Displacement node under the active Principled BSDF node
2805 disp_node
.location
= active_node
.location
+ Vector((100, -700))
2806 link
= links
.new(disp_node
.inputs
[0], disp_texture
.outputs
[0])
2808 # TODO Turn on true displacement in the material
2809 # Too complicated for now
2812 output_node
= [n
for n
in nodes
if n
.bl_idname
== 'ShaderNodeOutputMaterial']
2814 if not output_node
[0].inputs
[2].is_linked
:
2815 link
= links
.new(output_node
[0].inputs
[2], disp_node
.outputs
[0])
2819 # AMBIENT OCCLUSION TEXTURE
2820 if sname
[0] == 'Ambient Occlusion':
2821 ao_texture
= nodes
.new(type='ShaderNodeTexImage')
2822 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2823 ao_texture
.image
= img
2824 ao_texture
.label
= sname
[0]
2825 if ao_texture
.image
:
2826 ao_texture
.image
.colorspace_settings
.is_data
= True
2830 if not active_node
.inputs
[sname
[0]].is_linked
:
2831 # No texture node connected -> add texture node with new image
2832 texture_node
= nodes
.new(type='ShaderNodeTexImage')
2833 img
= bpy
.data
.images
.load(path
.join(import_path
, sname
[2]))
2834 texture_node
.image
= img
2837 if sname
[0] == 'Normal':
2838 # Test if new texture node is normal or bump map
2839 fname_components
= split_into__components(sname
[2])
2840 match_normal
= set(normal_abbr
).intersection(set(fname_components
))
2841 match_bump
= set(bump_abbr
).intersection(set(fname_components
))
2843 # If Normal add normal node in between
2844 normal_node
= nodes
.new(type='ShaderNodeNormalMap')
2845 link
= links
.new(normal_node
.inputs
[1], texture_node
.outputs
[0])
2847 # If Bump add bump node in between
2848 normal_node
= nodes
.new(type='ShaderNodeBump')
2849 link
= links
.new(normal_node
.inputs
[2], texture_node
.outputs
[0])
2851 link
= links
.new(active_node
.inputs
[sname
[0]], normal_node
.outputs
[0])
2852 normal_node_texture
= texture_node
2854 elif sname
[0] == 'Roughness':
2855 # Test if glossy or roughness map
2856 fname_components
= split_into__components(sname
[2])
2857 match_rough
= set(rough_abbr
).intersection(set(fname_components
))
2858 match_gloss
= set(gloss_abbr
).intersection(set(fname_components
))
2861 # If Roughness nothing to to
2862 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2865 # If Gloss Map add invert node
2866 invert_node
= nodes
.new(type='ShaderNodeInvert')
2867 link
= links
.new(invert_node
.inputs
[1], texture_node
.outputs
[0])
2869 link
= links
.new(active_node
.inputs
[sname
[0]], invert_node
.outputs
[0])
2870 roughness_node
= texture_node
2873 # This is a simple connection Texture --> Input slot
2874 link
= links
.new(active_node
.inputs
[sname
[0]], texture_node
.outputs
[0])
2876 # Use non-color for all but 'Base Color' Textures
2877 if not sname
[0] in ['Base Color', 'Emission'] and texture_node
.image
:
2878 texture_node
.image
.colorspace_settings
.is_data
= True
2881 # If already texture connected. add to node list for alignment
2882 texture_node
= active_node
.inputs
[sname
[0]].links
[0].from_node
2884 # This are all connected texture nodes
2885 texture_nodes
.append(texture_node
)
2886 texture_node
.label
= sname
[0]
2889 texture_nodes
.append(disp_texture
)
2892 # We want the ambient occlusion texture to be the top most texture node
2893 texture_nodes
.insert(0, ao_texture
)
2896 for i
, texture_node
in enumerate(texture_nodes
):
2897 offset
= Vector((-550, (i
* -280) + 200))
2898 texture_node
.location
= active_node
.location
+ offset
2901 # Extra alignment if normal node was added
2902 normal_node
.location
= normal_node_texture
.location
+ Vector((300, 0))
2905 # Alignment of invert node if glossy map
2906 invert_node
.location
= roughness_node
.location
+ Vector((300, 0))
2908 # Add texture input + mapping
2909 mapping
= nodes
.new(type='ShaderNodeMapping')
2910 mapping
.location
= active_node
.location
+ Vector((-1050, 0))
2911 if len(texture_nodes
) > 1:
2912 # If more than one texture add reroute node in between
2913 reroute
= nodes
.new(type='NodeReroute')
2914 texture_nodes
.append(reroute
)
2915 tex_coords
= Vector((texture_nodes
[0].location
.x
, sum(n
.location
.y
for n
in texture_nodes
)/len(texture_nodes
)))
2916 reroute
.location
= tex_coords
+ Vector((-50, -120))
2917 for texture_node
in texture_nodes
:
2918 link
= links
.new(texture_node
.inputs
[0], reroute
.outputs
[0])
2919 link
= links
.new(reroute
.inputs
[0], mapping
.outputs
[0])
2921 link
= links
.new(texture_nodes
[0].inputs
[0], mapping
.outputs
[0])
2923 # Connect texture_coordiantes to mapping node
2924 texture_input
= nodes
.new(type='ShaderNodeTexCoord')
2925 texture_input
.location
= mapping
.location
+ Vector((-200, 0))
2926 link
= links
.new(mapping
.inputs
[0], texture_input
.outputs
[2])
2928 # Create frame around tex coords and mapping
2929 frame
= nodes
.new(type='NodeFrame')
2930 frame
.label
= 'Mapping'
2931 mapping
.parent
= frame
2932 texture_input
.parent
= frame
2935 # Create frame around texture nodes
2936 frame
= nodes
.new(type='NodeFrame')
2937 frame
.label
= 'Textures'
2938 for tnode
in texture_nodes
:
2939 tnode
.parent
= frame
2943 active_node
.select
= False
2946 force_update(context
)
2950 class NWAddReroutes(Operator
, NWBase
):
2951 """Add Reroute Nodes and link them to outputs of selected nodes"""
2952 bl_idname
= "node.nw_add_reroutes"
2953 bl_label
= "Add Reroutes"
2954 bl_description
= "Add Reroutes to Outputs"
2955 bl_options
= {'REGISTER', 'UNDO'}
2957 option
: EnumProperty(
2960 ('ALL', 'to all', 'Add to all outputs'),
2961 ('LOOSE', 'to loose', 'Add only to loose outputs'),
2962 ('LINKED', 'to linked', 'Add only to linked outputs'),
2966 def execute(self
, context
):
2967 tree_type
= context
.space_data
.node_tree
.type
2968 option
= self
.option
2969 nodes
, links
= get_nodes_links(context
)
2970 # output valid when option is 'all' or when 'loose' output has no links
2972 post_select
= [] # nodes to be selected after execution
2973 # create reroutes and recreate links
2974 for node
in [n
for n
in nodes
if n
.select
]:
2979 # unhide 'REROUTE' nodes to avoid issues with location.y
2980 if node
.type == 'REROUTE':
2982 # When node is hidden - width_hidden not usable.
2983 # Hack needed to calculate real width
2985 bpy
.ops
.node
.select_all(action
='DESELECT')
2986 helper
= nodes
.new('NodeReroute')
2987 helper
.select
= True
2989 # resize node and helper to zero. Then check locations to calculate width
2990 bpy
.ops
.transform
.resize(value
=(0.0, 0.0, 0.0))
2991 width
= 2.0 * (helper
.location
.x
- node
.location
.x
)
2992 # restore node location
2993 node
.location
= x
, y
2996 # only helper is selected now
2997 bpy
.ops
.node
.delete()
2998 x
= node
.location
.x
+ width
+ 20.0
2999 if node
.type != 'REROUTE':
3003 reroutes_count
= 0 # will be used when aligning reroutes added to hidden nodes
3004 for out_i
, output
in enumerate(node
.outputs
):
3005 pass_used
= False # initial value to be analyzed if 'R_LAYERS'
3006 # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
3007 if node
.type != 'R_LAYERS':
3009 else: # if 'R_LAYERS' check if output represent used render pass
3010 node_scene
= node
.scene
3011 node_layer
= node
.layer
3012 # If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
3013 if output
.name
== 'Alpha':
3016 # check entries in global 'rl_outputs' variable
3017 for rlo
in rl_outputs
:
3018 if output
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3019 pass_used
= getattr(node_scene
.view_layers
[node_layer
], rlo
.render_pass
)
3022 valid
= ((option
== 'ALL') or
3023 (option
== 'LOOSE' and not output
.links
) or
3024 (option
== 'LINKED' and output
.links
))
3025 # Add reroutes only if valid, but offset location in all cases.
3027 n
= nodes
.new('NodeReroute')
3029 for link
in output
.links
:
3030 links
.new(n
.outputs
[0], link
.to_socket
)
3031 links
.new(output
, n
.inputs
[0])
3033 post_select
.append(n
)
3037 # disselect the node so that after execution of script only newly created nodes are selected
3039 # nicer reroutes distribution along y when node.hide
3041 y_translate
= reroutes_count
* y_offset
/ 2.0 - y_offset
- 35.0
3042 for reroute
in [r
for r
in nodes
if r
.select
]:
3043 reroute
.location
.y
-= y_translate
3044 for node
in post_select
:
3050 class NWLinkActiveToSelected(Operator
, NWBase
):
3051 """Link active node to selected nodes basing on various criteria"""
3052 bl_idname
= "node.nw_link_active_to_selected"
3053 bl_label
= "Link Active Node to Selected"
3054 bl_options
= {'REGISTER', 'UNDO'}
3056 replace
: BoolProperty()
3057 use_node_name
: BoolProperty()
3058 use_outputs_names
: BoolProperty()
3061 def poll(cls
, context
):
3063 if nw_check(context
):
3064 if context
.active_node
is not None:
3065 if context
.active_node
.select
:
3069 def execute(self
, context
):
3070 nodes
, links
= get_nodes_links(context
)
3071 replace
= self
.replace
3072 use_node_name
= self
.use_node_name
3073 use_outputs_names
= self
.use_outputs_names
3074 active
= nodes
.active
3075 selected
= [node
for node
in nodes
if node
.select
and node
!= active
]
3076 outputs
= [] # Only usable outputs of active nodes will be stored here.
3077 for out
in active
.outputs
:
3078 if active
.type != 'R_LAYERS':
3081 # 'R_LAYERS' node type needs special handling.
3082 # outputs of 'R_LAYERS' are callable even if not seen in UI.
3083 # Only outputs that represent used passes should be taken into account
3084 # Check if pass represented by output is used.
3085 # global 'rl_outputs' list will be used for that
3086 for rlo
in rl_outputs
:
3087 pass_used
= False # initial value. Will be set to True if pass is used
3088 if out
.name
== 'Alpha':
3089 # Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
3091 elif out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3092 # example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
3093 pass_used
= getattr(active
.scene
.view_layers
[active
.layer
], rlo
.render_pass
)
3097 doit
= True # Will be changed to False when links successfully added to previous output.
3100 for node
in selected
:
3101 dst_name
= node
.name
# Will be compared with src_name if needed.
3102 # When node has label - use it as dst_name
3104 dst_name
= node
.label
3105 valid
= True # Initial value. Will be changed to False if names don't match.
3106 src_name
= dst_name
# If names not used - this assignment will keep valid = True.
3108 # Set src_name to source node name or label
3109 src_name
= active
.name
3111 src_name
= active
.label
3112 elif use_outputs_names
:
3113 src_name
= (out
.name
, )
3114 for rlo
in rl_outputs
:
3115 if out
.name
in {rlo
.output_name
, rlo
.exr_output_name
}:
3116 src_name
= (rlo
.output_name
, rlo
.exr_output_name
)
3117 if dst_name
not in src_name
:
3120 for input in node
.inputs
:
3121 if input.type == out
.type or node
.type == 'REROUTE':
3122 if replace
or not input.is_linked
:
3123 links
.new(out
, input)
3124 if not use_node_name
and not use_outputs_names
:
3131 class NWAlignNodes(Operator
, NWBase
):
3132 '''Align the selected nodes neatly in a row/column'''
3133 bl_idname
= "node.nw_align_nodes"
3134 bl_label
= "Align Nodes"
3135 bl_options
= {'REGISTER', 'UNDO'}
3136 margin
: IntProperty(name
='Margin', default
=50, description
='The amount of space between nodes')
3138 def execute(self
, context
):
3139 nodes
, links
= get_nodes_links(context
)
3140 margin
= self
.margin
3144 if node
.select
and node
.type != 'FRAME':
3145 selection
.append(node
)
3147 # If no nodes are selected, align all nodes
3151 elif nodes
.active
in selection
:
3152 active_loc
= copy(nodes
.active
.location
) # make a copy, not a reference
3154 # Check if nodes should be laid out horizontally or vertically
3155 x_locs
= [n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
] # use dimension to get center of node, not corner
3156 y_locs
= [n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
]
3157 x_range
= max(x_locs
) - min(x_locs
)
3158 y_range
= max(y_locs
) - min(y_locs
)
3159 mid_x
= (max(x_locs
) + min(x_locs
)) / 2
3160 mid_y
= (max(y_locs
) + min(y_locs
)) / 2
3161 horizontal
= x_range
> y_range
3163 # Sort selection by location of node mid-point
3165 selection
= sorted(selection
, key
=lambda n
: n
.location
.x
+ (n
.dimensions
.x
/ 2))
3167 selection
= sorted(selection
, key
=lambda n
: n
.location
.y
- (n
.dimensions
.y
/ 2), reverse
=True)
3171 for node
in selection
:
3172 current_margin
= margin
3173 current_margin
= current_margin
* 0.5 if node
.hide
else current_margin
# use a smaller margin for hidden nodes
3176 node
.location
.x
= current_pos
3177 current_pos
+= current_margin
+ node
.dimensions
.x
3178 node
.location
.y
= mid_y
+ (node
.dimensions
.y
/ 2)
3180 node
.location
.y
= current_pos
3181 current_pos
-= (current_margin
* 0.3) + node
.dimensions
.y
# use half-margin for vertical alignment
3182 node
.location
.x
= mid_x
- (node
.dimensions
.x
/ 2)
3184 # If active node is selected, center nodes around it
3185 if active_loc
is not None:
3186 active_loc_diff
= active_loc
- nodes
.active
.location
3187 for node
in selection
:
3188 node
.location
+= active_loc_diff
3189 else: # Position nodes centered around where they used to be
3190 locs
= ([n
.location
.x
+ (n
.dimensions
.x
/ 2) for n
in selection
]) if horizontal
else ([n
.location
.y
- (n
.dimensions
.y
/ 2) for n
in selection
])
3191 new_mid
= (max(locs
) + min(locs
)) / 2
3192 for node
in selection
:
3194 node
.location
.x
+= (mid_x
- new_mid
)
3196 node
.location
.y
+= (mid_y
- new_mid
)
3201 class NWSelectParentChildren(Operator
, NWBase
):
3202 bl_idname
= "node.nw_select_parent_child"
3203 bl_label
= "Select Parent or Children"
3204 bl_options
= {'REGISTER', 'UNDO'}
3206 option
: EnumProperty(
3209 ('PARENT', 'Select Parent', 'Select Parent Frame'),
3210 ('CHILD', 'Select Children', 'Select members of selected frame'),
3214 def execute(self
, context
):
3215 nodes
, links
= get_nodes_links(context
)
3216 option
= self
.option
3217 selected
= [node
for node
in nodes
if node
.select
]
3218 if option
== 'PARENT':
3219 for sel
in selected
:
3222 parent
.select
= True
3223 else: # option == 'CHILD'
3224 for sel
in selected
:
3225 children
= [node
for node
in nodes
if node
.parent
== sel
]
3226 for kid
in children
:
3232 class NWDetachOutputs(Operator
, NWBase
):
3233 """Detach outputs of selected node leaving inputs linked"""
3234 bl_idname
= "node.nw_detach_outputs"
3235 bl_label
= "Detach Outputs"
3236 bl_options
= {'REGISTER', 'UNDO'}
3238 def execute(self
, context
):
3239 nodes
, links
= get_nodes_links(context
)
3240 selected
= context
.selected_nodes
3241 bpy
.ops
.node
.duplicate_move_keep_inputs()
3242 new_nodes
= context
.selected_nodes
3243 bpy
.ops
.node
.select_all(action
="DESELECT")
3244 for node
in selected
:
3246 bpy
.ops
.node
.delete_reconnect()
3247 for new_node
in new_nodes
:
3248 new_node
.select
= True
3249 bpy
.ops
.transform
.translate('INVOKE_DEFAULT')
3254 class NWLinkToOutputNode(Operator
):
3255 """Link to Composite node or Material Output node"""
3256 bl_idname
= "node.nw_link_out"
3257 bl_label
= "Connect to Output"
3258 bl_options
= {'REGISTER', 'UNDO'}
3261 def poll(cls
, context
):
3263 if nw_check(context
):
3264 if context
.active_node
is not None:
3265 for out
in context
.active_node
.outputs
:
3266 if is_visible_socket(out
):
3271 def execute(self
, context
):
3272 nodes
, links
= get_nodes_links(context
)
3273 active
= nodes
.active
3275 tree_type
= context
.space_data
.tree_type
3276 shader_outputs
= {'OBJECT': 'ShaderNodeOutputMaterial',
3277 'WORLD': 'ShaderNodeOutputWorld',
3278 'LINESTYLE': 'ShaderNodeOutputLineStyle'}
3280 'ShaderNodeTree': shader_outputs
[context
.space_data
.shader_type
],
3281 'CompositorNodeTree': 'CompositorNodeComposite',
3282 'TextureNodeTree': 'TextureNodeOutput',
3283 'GeometryNodeTree': 'NodeGroupOutput',
3286 # check whether the node is an output node and,
3287 # if supported, whether it's the active one
3288 if node
.rna_type
.identifier
== output_type \
3289 and (node
.is_active_output
if hasattr(node
, 'is_active_output')
3293 else: # No output node exists
3294 bpy
.ops
.node
.select_all(action
="DESELECT")
3295 output_node
= nodes
.new(output_type
)
3296 output_node
.location
.x
= active
.location
.x
+ active
.dimensions
.x
+ 80
3297 output_node
.location
.y
= active
.location
.y
3300 for i
, output
in enumerate(active
.outputs
):
3301 if is_visible_socket(output
):
3304 for i
, output
in enumerate(active
.outputs
):
3305 if output
.type == output_node
.inputs
[0].type and is_visible_socket(output
):
3310 if tree_type
== 'ShaderNodeTree':
3311 if active
.outputs
[output_index
].name
== 'Volume':
3313 elif active
.outputs
[output_index
].name
== 'Displacement':
3315 elif tree_type
== 'GeometryNodeTree':
3316 if active
.outputs
[output_index
].type != 'GEOMETRY':
3317 return {'CANCELLED'}
3318 links
.new(active
.outputs
[output_index
], output_node
.inputs
[out_input_index
])
3320 force_update(context
) # viewport render does not update
3325 class NWMakeLink(Operator
, NWBase
):
3326 """Make a link from one socket to another"""
3327 bl_idname
= 'node.nw_make_link'
3328 bl_label
= 'Make Link'
3329 bl_options
= {'REGISTER', 'UNDO'}
3330 from_socket
: IntProperty()
3331 to_socket
: IntProperty()
3333 def execute(self
, context
):
3334 nodes
, links
= get_nodes_links(context
)
3336 n1
= nodes
[context
.scene
.NWLazySource
]
3337 n2
= nodes
[context
.scene
.NWLazyTarget
]
3339 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[self
.to_socket
])
3341 force_update(context
)
3346 class NWCallInputsMenu(Operator
, NWBase
):
3347 """Link from this output"""
3348 bl_idname
= 'node.nw_call_inputs_menu'
3349 bl_label
= 'Make Link'
3350 bl_options
= {'REGISTER', 'UNDO'}
3351 from_socket
: IntProperty()
3353 def execute(self
, context
):
3354 nodes
, links
= get_nodes_links(context
)
3356 context
.scene
.NWSourceSocket
= self
.from_socket
3358 n1
= nodes
[context
.scene
.NWLazySource
]
3359 n2
= nodes
[context
.scene
.NWLazyTarget
]
3360 if len(n2
.inputs
) > 1:
3361 bpy
.ops
.wm
.call_menu("INVOKE_DEFAULT", name
=NWConnectionListInputs
.bl_idname
)
3362 elif len(n2
.inputs
) == 1:
3363 links
.new(n1
.outputs
[self
.from_socket
], n2
.inputs
[0])
3367 class NWAddSequence(Operator
, NWBase
, ImportHelper
):
3368 """Add an Image Sequence"""
3369 bl_idname
= 'node.nw_add_sequence'
3370 bl_label
= 'Import Image Sequence'
3371 bl_options
= {'REGISTER', 'UNDO'}
3373 directory
: StringProperty(
3376 filename
: StringProperty(
3379 files
: CollectionProperty(
3380 type=bpy
.types
.OperatorFileListElement
,
3381 options
={'HIDDEN', 'SKIP_SAVE'}
3383 relative_path
: BoolProperty(
3384 name
='Relative Path',
3385 description
='Set the file path relative to the blend file, when possible',
3389 def draw(self
, context
):
3390 layout
= self
.layout
3391 layout
.alignment
= 'LEFT'
3393 layout
.prop(self
, 'relative_path')
3395 def execute(self
, context
):
3396 nodes
, links
= get_nodes_links(context
)
3397 directory
= self
.directory
3398 filename
= self
.filename
3400 tree
= context
.space_data
.node_tree
3403 # print ("\nDIR:", directory)
3404 # print ("FN:", filename)
3405 # print ("Fs:", list(f.name for f in files), '\n')
3407 if tree
.type == 'SHADER':
3408 node_type
= "ShaderNodeTexImage"
3409 elif tree
.type == 'COMPOSITING':
3410 node_type
= "CompositorNodeImage"
3412 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3413 return {'CANCELLED'}
3415 if not files
[0].name
and not filename
:
3416 self
.report({'ERROR'}, "No file chosen")
3417 return {'CANCELLED'}
3418 elif files
[0].name
and (not filename
or not path
.exists(directory
+filename
)):
3419 # User has selected multiple files without an active one, or the active one is non-existent
3420 filename
= files
[0].name
3422 if not path
.exists(directory
+filename
):
3423 self
.report({'ERROR'}, filename
+" does not exist!")
3424 return {'CANCELLED'}
3426 without_ext
= '.'.join(filename
.split('.')[:-1])
3428 # if last digit isn't a number, it's not a sequence
3429 if not without_ext
[-1].isdigit():
3430 self
.report({'ERROR'}, filename
+" does not seem to be part of a sequence")
3431 return {'CANCELLED'}
3434 extension
= filename
.split('.')[-1]
3435 reverse
= without_ext
[::-1] # reverse string
3438 for char
in reverse
:
3444 without_num
= without_ext
[:count_numbers
*-1]
3446 files
= sorted(glob(directory
+ without_num
+ "[0-9]"*count_numbers
+ "." + extension
))
3448 num_frames
= len(files
)
3450 nodes_list
= [node
for node
in nodes
]
3452 nodes_list
.sort(key
=lambda k
: k
.location
.x
)
3453 xloc
= nodes_list
[0].location
.x
- 220 # place new nodes at far left
3457 yloc
+= node_mid_pt(node
, 'y')
3458 yloc
= yloc
/len(nodes
)
3463 name_with_hashes
= without_num
+ "#"*count_numbers
+ '.' + extension
3465 bpy
.ops
.node
.add_node('INVOKE_DEFAULT', use_transform
=True, type=node_type
)
3467 node
.label
= name_with_hashes
3469 filepath
= directory
+(without_ext
+'.'+extension
)
3470 if self
.relative_path
:
3471 if bpy
.data
.filepath
:
3473 filepath
= bpy
.path
.relpath(filepath
)
3477 img
= bpy
.data
.images
.load(filepath
)
3478 img
.source
= 'SEQUENCE'
3479 img
.name
= name_with_hashes
3481 image_user
= node
.image_user
if tree
.type == 'SHADER' else node
3482 image_user
.frame_offset
= int(files
[0][len(without_num
)+len(directory
):-1*(len(extension
)+1)]) - 1 # separate the number from the file name of the first file
3483 image_user
.frame_duration
= num_frames
3488 class NWAddMultipleImages(Operator
, NWBase
, ImportHelper
):
3489 """Add multiple images at once"""
3490 bl_idname
= 'node.nw_add_multiple_images'
3491 bl_label
= 'Open Selected Images'
3492 bl_options
= {'REGISTER', 'UNDO'}
3493 directory
: StringProperty(
3496 files
: CollectionProperty(
3497 type=bpy
.types
.OperatorFileListElement
,
3498 options
={'HIDDEN', 'SKIP_SAVE'}
3501 def execute(self
, context
):
3502 nodes
, links
= get_nodes_links(context
)
3504 xloc
, yloc
= context
.region
.view2d
.region_to_view(context
.area
.width
/2, context
.area
.height
/2)
3506 if context
.space_data
.node_tree
.type == 'SHADER':
3507 node_type
= "ShaderNodeTexImage"
3508 elif context
.space_data
.node_tree
.type == 'COMPOSITING':
3509 node_type
= "CompositorNodeImage"
3511 self
.report({'ERROR'}, "Unsupported Node Tree type!")
3512 return {'CANCELLED'}
3515 for f
in self
.files
:
3518 node
= nodes
.new(node_type
)
3519 new_nodes
.append(node
)
3522 node
.width_hidden
= 100
3523 node
.location
.x
= xloc
3524 node
.location
.y
= yloc
3527 img
= bpy
.data
.images
.load(self
.directory
+fname
)
3530 # shift new nodes up to center of tree
3531 list_size
= new_nodes
[0].location
.y
- new_nodes
[-1].location
.y
3533 if node
in new_nodes
:
3535 node
.location
.y
+= (list_size
/2)
3541 class NWViewerFocus(bpy
.types
.Operator
):
3542 """Set the viewer tile center to the mouse position"""
3543 bl_idname
= "node.nw_viewer_focus"
3544 bl_label
= "Viewer Focus"
3546 x
: bpy
.props
.IntProperty()
3547 y
: bpy
.props
.IntProperty()
3550 def poll(cls
, context
):
3551 return nw_check(context
) and context
.space_data
.tree_type
== 'CompositorNodeTree'
3553 def execute(self
, context
):
3556 def invoke(self
, context
, event
):
3557 render
= context
.scene
.render
3558 space
= context
.space_data
3559 percent
= render
.resolution_percentage
*0.01
3561 nodes
, links
= get_nodes_links(context
)
3562 viewers
= [n
for n
in nodes
if n
.type == 'VIEWER']
3565 mlocx
= event
.mouse_region_x
3566 mlocy
= event
.mouse_region_y
3567 select_node
= bpy
.ops
.node
.select(location
=(mlocx
, mlocy
), extend
=False)
3569 if not 'FINISHED' in select_node
: # only run if we're not clicking on a node
3570 region_x
= context
.region
.width
3571 region_y
= context
.region
.height
3573 region_center_x
= context
.region
.width
/ 2
3574 region_center_y
= context
.region
.height
/ 2
3576 bd_x
= render
.resolution_x
* percent
* space
.backdrop_zoom
3577 bd_y
= render
.resolution_y
* percent
* space
.backdrop_zoom
3579 backdrop_center_x
= (bd_x
/ 2) - space
.backdrop_offset
[0]
3580 backdrop_center_y
= (bd_y
/ 2) - space
.backdrop_offset
[1]
3582 margin_x
= region_center_x
- backdrop_center_x
3583 margin_y
= region_center_y
- backdrop_center_y
3585 abs_mouse_x
= (mlocx
- margin_x
) / bd_x
3586 abs_mouse_y
= (mlocy
- margin_y
) / bd_y
3588 for node
in viewers
:
3589 node
.center_x
= abs_mouse_x
3590 node
.center_y
= abs_mouse_y
3592 return {'PASS_THROUGH'}
3594 return self
.execute(context
)
3597 class NWSaveViewer(bpy
.types
.Operator
, ExportHelper
):
3598 """Save the current viewer node to an image file"""
3599 bl_idname
= "node.nw_save_viewer"
3600 bl_label
= "Save This Image"
3601 filepath
: StringProperty(subtype
="FILE_PATH")
3602 filename_ext
: EnumProperty(
3604 description
="Choose the file format to save to",
3605 items
=(('.bmp', "BMP", ""),
3606 ('.rgb', 'IRIS', ""),
3607 ('.png', 'PNG', ""),
3608 ('.jpg', 'JPEG', ""),
3609 ('.jp2', 'JPEG2000', ""),
3610 ('.tga', 'TARGA', ""),
3611 ('.cin', 'CINEON', ""),
3612 ('.dpx', 'DPX', ""),
3613 ('.exr', 'OPEN_EXR', ""),
3614 ('.hdr', 'HDR', ""),
3615 ('.tif', 'TIFF', "")),
3620 def poll(cls
, context
):
3622 if nw_check(context
):
3623 if context
.space_data
.tree_type
== 'CompositorNodeTree':
3624 if "Viewer Node" in [i
.name
for i
in bpy
.data
.images
]:
3625 if sum(bpy
.data
.images
["Viewer Node"].size
) > 0: # False if not connected or connected but no image
3629 def execute(self
, context
):
3646 basename
, ext
= path
.splitext(fp
)
3647 old_render_format
= context
.scene
.render
.image_settings
.file_format
3648 context
.scene
.render
.image_settings
.file_format
= formats
[self
.filename_ext
]
3649 context
.area
.type = "IMAGE_EDITOR"
3650 context
.area
.spaces
[0].image
= bpy
.data
.images
['Viewer Node']
3651 context
.area
.spaces
[0].image
.save_render(fp
)
3652 context
.area
.type = "NODE_EDITOR"
3653 context
.scene
.render
.image_settings
.file_format
= old_render_format
3657 class NWResetNodes(bpy
.types
.Operator
):
3658 """Reset Nodes in Selection"""
3659 bl_idname
= "node.nw_reset_nodes"
3660 bl_label
= "Reset Nodes"
3661 bl_options
= {'REGISTER', 'UNDO'}
3664 def poll(cls
, context
):
3665 space
= context
.space_data
3666 return space
.type == 'NODE_EDITOR'
3668 def execute(self
, context
):
3669 node_active
= context
.active_node
3670 node_selected
= context
.selected_nodes
3671 node_ignore
= ["FRAME","REROUTE", "GROUP"]
3673 # Check if one node is selected at least
3674 if not (len(node_selected
) > 0):
3675 self
.report({'ERROR'}, "1 node must be selected at least")
3676 return {'CANCELLED'}
3678 active_node_name
= node_active
.name
if node_active
.select
else None
3679 valid_nodes
= [n
for n
in node_selected
if n
.type not in node_ignore
]
3681 # Create output lists
3682 selected_node_names
= [n
.name
for n
in node_selected
]
3685 # Reset all valid children in a frame
3686 node_active_is_frame
= False
3687 if len(node_selected
) == 1 and node_active
.type == "FRAME":
3688 node_tree
= node_active
.id_data
3689 children
= [n
for n
in node_tree
.nodes
if n
.parent
== node_active
]
3691 valid_nodes
= [n
for n
in children
if n
.type not in node_ignore
]
3692 selected_node_names
= [n
.name
for n
in children
if n
.type not in node_ignore
]
3693 node_active_is_frame
= True
3695 # Check if valid nodes in selection
3696 if not (len(valid_nodes
) > 0):
3697 # Check for frames only
3698 frames_selected
= [n
for n
in node_selected
if n
.type == "FRAME"]
3699 if (len(frames_selected
) > 1 and len(frames_selected
) == len(node_selected
)):
3700 self
.report({'ERROR'}, "Please select only 1 frame to reset")
3702 self
.report({'ERROR'}, "No valid node(s) in selection")
3703 return {'CANCELLED'}
3705 # Report nodes that are not valid
3706 if len(valid_nodes
) != len(node_selected
) and node_active_is_frame
is False:
3707 valid_node_names
= [n
.name
for n
in valid_nodes
]
3708 not_valid_names
= list(set(selected_node_names
) - set(valid_node_names
))
3709 self
.report({'INFO'}, "Ignored {}".format(", ".join(not_valid_names
)))
3711 # Deselect all nodes
3712 for i
in node_selected
:
3715 # Run through all valid nodes
3716 for node
in valid_nodes
:
3718 parent
= node
.parent
if node
.parent
else None
3719 node_loc
= [node
.location
.x
, node
.location
.y
]
3721 node_tree
= node
.id_data
3722 props_to_copy
= 'bl_idname name location height width'.split(' ')
3725 mappings
= chain
.from_iterable([node
.inputs
, node
.outputs
])
3726 for i
in (i
for i
in mappings
if i
.is_linked
):
3728 reconnections
.append([L
.from_socket
.path_from_id(), L
.to_socket
.path_from_id()])
3730 props
= {j
: getattr(node
, j
) for j
in props_to_copy
}
3732 new_node
= node_tree
.nodes
.new(props
['bl_idname'])
3733 props_to_copy
.pop(0)
3735 for prop
in props_to_copy
:
3736 setattr(new_node
, prop
, props
[prop
])
3738 nodes
= node_tree
.nodes
3740 new_node
.name
= props
['name']
3743 new_node
.parent
= parent
3744 new_node
.location
= node_loc
3746 for str_from
, str_to
in reconnections
:
3747 node_tree
.links
.new(eval(str_from
), eval(str_to
))
3749 new_node
.select
= False
3750 success_names
.append(new_node
.name
)
3752 # Reselect all nodes
3753 if selected_node_names
and node_active_is_frame
is False:
3754 for i
in selected_node_names
:
3755 node_tree
.nodes
[i
].select
= True
3757 if active_node_name
is not None:
3758 node_tree
.nodes
[active_node_name
].select
= True
3759 node_tree
.nodes
.active
= node_tree
.nodes
[active_node_name
]
3761 self
.report({'INFO'}, "Successfully reset {}".format(", ".join(success_names
)))
3769 def drawlayout(context
, layout
, mode
='non-panel'):
3770 tree_type
= context
.space_data
.tree_type
3772 col
= layout
.column(align
=True)
3773 col
.menu(NWMergeNodesMenu
.bl_idname
)
3776 col
= layout
.column(align
=True)
3777 col
.menu(NWSwitchNodeTypeMenu
.bl_idname
, text
="Switch Node Type")
3780 if tree_type
== 'ShaderNodeTree':
3781 col
= layout
.column(align
=True)
3782 col
.operator(NWAddTextureSetup
.bl_idname
, text
="Add Texture Setup", icon
='NODE_SEL')
3783 col
.operator(NWAddPrincipledSetup
.bl_idname
, text
="Add Principled Setup", icon
='NODE_SEL')
3786 col
= layout
.column(align
=True)
3787 col
.operator(NWDetachOutputs
.bl_idname
, icon
='UNLINKED')
3788 col
.operator(NWSwapLinks
.bl_idname
)
3789 col
.menu(NWAddReroutesMenu
.bl_idname
, text
="Add Reroutes", icon
='LAYER_USED')
3792 col
= layout
.column(align
=True)
3793 col
.menu(NWLinkActiveToSelectedMenu
.bl_idname
, text
="Link Active To Selected", icon
='LINKED')
3794 if tree_type
!= 'GeometryNodeTree':
3795 col
.operator(NWLinkToOutputNode
.bl_idname
, icon
='DRIVER')
3798 col
= layout
.column(align
=True)
3800 row
= col
.row(align
=True)
3801 row
.operator(NWClearLabel
.bl_idname
).option
= True
3802 row
.operator(NWModifyLabels
.bl_idname
)
3804 col
.operator(NWClearLabel
.bl_idname
).option
= True
3805 col
.operator(NWModifyLabels
.bl_idname
)
3806 col
.menu(NWBatchChangeNodesMenu
.bl_idname
, text
="Batch Change")
3808 col
.menu(NWCopyToSelectedMenu
.bl_idname
, text
="Copy to Selected")
3811 col
= layout
.column(align
=True)
3812 if tree_type
== 'CompositorNodeTree':
3813 col
.operator(NWResetBG
.bl_idname
, icon
='ZOOM_PREVIOUS')
3814 if tree_type
!= 'GeometryNodeTree':
3815 col
.operator(NWReloadImages
.bl_idname
, icon
='FILE_REFRESH')
3818 col
= layout
.column(align
=True)
3819 col
.operator(NWFrameSelected
.bl_idname
, icon
='STICKY_UVS_LOC')
3822 col
= layout
.column(align
=True)
3823 col
.operator(NWAlignNodes
.bl_idname
, icon
='CENTER_ONLY')
3826 col
= layout
.column(align
=True)
3827 col
.operator(NWDeleteUnused
.bl_idname
, icon
='CANCEL')
3831 class NodeWranglerPanel(Panel
, NWBase
):
3832 bl_idname
= "NODE_PT_nw_node_wrangler"
3833 bl_space_type
= 'NODE_EDITOR'
3834 bl_label
= "Node Wrangler"
3835 bl_region_type
= "UI"
3836 bl_category
= "Node Wrangler"
3838 prepend
: StringProperty(
3841 append
: StringProperty()
3842 remove
: StringProperty()
3844 def draw(self
, context
):
3845 self
.layout
.label(text
="(Quick access: Shift+W)")
3846 drawlayout(context
, self
.layout
, mode
='panel')
3852 class NodeWranglerMenu(Menu
, NWBase
):
3853 bl_idname
= "NODE_MT_nw_node_wrangler_menu"
3854 bl_label
= "Node Wrangler"
3856 def draw(self
, context
):
3857 self
.layout
.operator_context
= 'INVOKE_DEFAULT'
3858 drawlayout(context
, self
.layout
)
3861 class NWMergeNodesMenu(Menu
, NWBase
):
3862 bl_idname
= "NODE_MT_nw_merge_nodes_menu"
3863 bl_label
= "Merge Selected Nodes"
3865 def draw(self
, context
):
3866 type = context
.space_data
.tree_type
3867 layout
= self
.layout
3868 if type == 'ShaderNodeTree':
3869 layout
.menu(NWMergeShadersMenu
.bl_idname
, text
="Use Shaders")
3870 if type == 'GeometryNodeTree':
3871 layout
.menu(NWMergeGeometryMenu
.bl_idname
, text
="Use Geometry Nodes")
3872 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
3874 layout
.menu(NWMergeMixMenu
.bl_idname
, text
="Use Mix Nodes")
3875 layout
.menu(NWMergeMathMenu
.bl_idname
, text
="Use Math Nodes")
3876 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Z-Combine Nodes")
3878 props
.merge_type
= 'ZCOMBINE'
3879 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
="Use Alpha Over Nodes")
3881 props
.merge_type
= 'ALPHAOVER'
3883 class NWMergeGeometryMenu(Menu
, NWBase
):
3884 bl_idname
= "NODE_MT_nw_merge_geometry_menu"
3885 bl_label
= "Merge Selected Nodes using Geometry Nodes"
3886 def draw(self
, context
):
3887 layout
= self
.layout
3888 # The boolean node + Join Geometry node
3889 for type, name
, description
in geo_combine_operations
:
3890 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3892 props
.merge_type
= 'GEOMETRY'
3894 class NWMergeShadersMenu(Menu
, NWBase
):
3895 bl_idname
= "NODE_MT_nw_merge_shaders_menu"
3896 bl_label
= "Merge Selected Nodes using Shaders"
3898 def draw(self
, context
):
3899 layout
= self
.layout
3900 for type in ('MIX', 'ADD'):
3901 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=type)
3903 props
.merge_type
= 'SHADER'
3906 class NWMergeMixMenu(Menu
, NWBase
):
3907 bl_idname
= "NODE_MT_nw_merge_mix_menu"
3908 bl_label
= "Merge Selected Nodes using Mix"
3910 def draw(self
, context
):
3911 layout
= self
.layout
3912 for type, name
, description
in blend_types
:
3913 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3915 props
.merge_type
= 'MIX'
3918 class NWConnectionListOutputs(Menu
, NWBase
):
3919 bl_idname
= "NODE_MT_nw_connection_list_out"
3922 def draw(self
, context
):
3923 layout
= self
.layout
3924 nodes
, links
= get_nodes_links(context
)
3926 n1
= nodes
[context
.scene
.NWLazySource
]
3927 for index
, output
in enumerate(n1
.outputs
):
3928 # Only show sockets that are exposed.
3930 layout
.operator(NWCallInputsMenu
.bl_idname
, text
=output
.name
, icon
="RADIOBUT_OFF").from_socket
=index
3933 class NWConnectionListInputs(Menu
, NWBase
):
3934 bl_idname
= "NODE_MT_nw_connection_list_in"
3937 def draw(self
, context
):
3938 layout
= self
.layout
3939 nodes
, links
= get_nodes_links(context
)
3941 n2
= nodes
[context
.scene
.NWLazyTarget
]
3943 for index
, input in enumerate(n2
.inputs
):
3944 # Only show sockets that are exposed.
3945 # This prevents, for example, the scale value socket
3946 # of the vector math node being added to the list when
3947 # the mode is not 'SCALE'.
3949 op
= layout
.operator(NWMakeLink
.bl_idname
, text
=input.name
, icon
="FORWARD")
3950 op
.from_socket
= context
.scene
.NWSourceSocket
3951 op
.to_socket
= index
3954 class NWMergeMathMenu(Menu
, NWBase
):
3955 bl_idname
= "NODE_MT_nw_merge_math_menu"
3956 bl_label
= "Merge Selected Nodes using Math"
3958 def draw(self
, context
):
3959 layout
= self
.layout
3960 for type, name
, description
in operations
:
3961 props
= layout
.operator(NWMergeNodes
.bl_idname
, text
=name
)
3963 props
.merge_type
= 'MATH'
3966 class NWBatchChangeNodesMenu(Menu
, NWBase
):
3967 bl_idname
= "NODE_MT_nw_batch_change_nodes_menu"
3968 bl_label
= "Batch Change Selected Nodes"
3970 def draw(self
, context
):
3971 layout
= self
.layout
3972 layout
.menu(NWBatchChangeBlendTypeMenu
.bl_idname
)
3973 layout
.menu(NWBatchChangeOperationMenu
.bl_idname
)
3976 class NWBatchChangeBlendTypeMenu(Menu
, NWBase
):
3977 bl_idname
= "NODE_MT_nw_batch_change_blend_type_menu"
3978 bl_label
= "Batch Change Blend Type"
3980 def draw(self
, context
):
3981 layout
= self
.layout
3982 for type, name
, description
in blend_types
:
3983 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3984 props
.blend_type
= type
3985 props
.operation
= 'CURRENT'
3988 class NWBatchChangeOperationMenu(Menu
, NWBase
):
3989 bl_idname
= "NODE_MT_nw_batch_change_operation_menu"
3990 bl_label
= "Batch Change Math Operation"
3992 def draw(self
, context
):
3993 layout
= self
.layout
3994 for type, name
, description
in operations
:
3995 props
= layout
.operator(NWBatchChangeNodes
.bl_idname
, text
=name
)
3996 props
.blend_type
= 'CURRENT'
3997 props
.operation
= type
4000 class NWCopyToSelectedMenu(Menu
, NWBase
):
4001 bl_idname
= "NODE_MT_nw_copy_node_properties_menu"
4002 bl_label
= "Copy to Selected"
4004 def draw(self
, context
):
4005 layout
= self
.layout
4006 layout
.operator(NWCopySettings
.bl_idname
, text
="Settings from Active")
4007 layout
.menu(NWCopyLabelMenu
.bl_idname
)
4010 class NWCopyLabelMenu(Menu
, NWBase
):
4011 bl_idname
= "NODE_MT_nw_copy_label_menu"
4012 bl_label
= "Copy Label"
4014 def draw(self
, context
):
4015 layout
= self
.layout
4016 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Active Node's Label").option
= 'FROM_ACTIVE'
4017 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Node's Label").option
= 'FROM_NODE'
4018 layout
.operator(NWCopyLabel
.bl_idname
, text
="from Linked Output's Name").option
= 'FROM_SOCKET'
4021 class NWAddReroutesMenu(Menu
, NWBase
):
4022 bl_idname
= "NODE_MT_nw_add_reroutes_menu"
4023 bl_label
= "Add Reroutes"
4024 bl_description
= "Add Reroute Nodes to Selected Nodes' Outputs"
4026 def draw(self
, context
):
4027 layout
= self
.layout
4028 layout
.operator(NWAddReroutes
.bl_idname
, text
="to All Outputs").option
= 'ALL'
4029 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Loose Outputs").option
= 'LOOSE'
4030 layout
.operator(NWAddReroutes
.bl_idname
, text
="to Linked Outputs").option
= 'LINKED'
4033 class NWLinkActiveToSelectedMenu(Menu
, NWBase
):
4034 bl_idname
= "NODE_MT_nw_link_active_to_selected_menu"
4035 bl_label
= "Link Active to Selected"
4037 def draw(self
, context
):
4038 layout
= self
.layout
4039 layout
.menu(NWLinkStandardMenu
.bl_idname
)
4040 layout
.menu(NWLinkUseNodeNameMenu
.bl_idname
)
4041 layout
.menu(NWLinkUseOutputsNamesMenu
.bl_idname
)
4044 class NWLinkStandardMenu(Menu
, NWBase
):
4045 bl_idname
= "NODE_MT_nw_link_standard_menu"
4046 bl_label
= "To All Selected"
4048 def draw(self
, context
):
4049 layout
= self
.layout
4050 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4051 props
.replace
= False
4052 props
.use_node_name
= False
4053 props
.use_outputs_names
= False
4054 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4055 props
.replace
= True
4056 props
.use_node_name
= False
4057 props
.use_outputs_names
= False
4060 class NWLinkUseNodeNameMenu(Menu
, NWBase
):
4061 bl_idname
= "NODE_MT_nw_link_use_node_name_menu"
4062 bl_label
= "Use Node Name/Label"
4064 def draw(self
, context
):
4065 layout
= self
.layout
4066 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4067 props
.replace
= False
4068 props
.use_node_name
= True
4069 props
.use_outputs_names
= False
4070 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4071 props
.replace
= True
4072 props
.use_node_name
= True
4073 props
.use_outputs_names
= False
4076 class NWLinkUseOutputsNamesMenu(Menu
, NWBase
):
4077 bl_idname
= "NODE_MT_nw_link_use_outputs_names_menu"
4078 bl_label
= "Use Outputs Names"
4080 def draw(self
, context
):
4081 layout
= self
.layout
4082 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Don't Replace Links")
4083 props
.replace
= False
4084 props
.use_node_name
= False
4085 props
.use_outputs_names
= True
4086 props
= layout
.operator(NWLinkActiveToSelected
.bl_idname
, text
="Replace Links")
4087 props
.replace
= True
4088 props
.use_node_name
= False
4089 props
.use_outputs_names
= True
4092 class NWAttributeMenu(bpy
.types
.Menu
):
4093 bl_idname
= "NODE_MT_nw_node_attribute_menu"
4094 bl_label
= "Attributes"
4097 def poll(cls
, context
):
4099 if nw_check(context
):
4100 snode
= context
.space_data
4101 valid
= snode
.tree_type
== 'ShaderNodeTree'
4104 def draw(self
, context
):
4106 nodes
, links
= get_nodes_links(context
)
4107 mat
= context
.object.active_material
4110 for obj
in bpy
.data
.objects
:
4111 for slot
in obj
.material_slots
:
4112 if slot
.material
== mat
:
4116 if obj
.data
.attributes
:
4117 for attr
in obj
.data
.attributes
:
4118 attrs
.append(attr
.name
)
4119 attrs
= list(set(attrs
)) # get a unique list
4123 l
.operator(NWAddAttrNode
.bl_idname
, text
=attr
).attr_name
= attr
4125 l
.label(text
="No attributes on objects with this material")
4128 class NWSwitchNodeTypeMenu(Menu
, NWBase
):
4129 bl_idname
= "NODE_MT_nw_switch_node_type_menu"
4130 bl_label
= "Switch Type to..."
4132 def draw(self
, context
):
4133 layout
= self
.layout
4134 categories
= [c
for c
in node_categories_iter(context
)
4135 if c
.name
not in ['Group', 'Script']]
4136 for cat
in categories
:
4137 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4138 if hasattr(bpy
.types
, idname
):
4141 layout
.label(text
="Unable to load altered node lists.")
4142 layout
.label(text
="Please re-enable Node Wrangler.")
4146 def draw_switch_category_submenu(self
, context
):
4147 layout
= self
.layout
4148 if self
.category
.name
== 'Layout':
4149 for node
in self
.category
.items(context
):
4150 if node
.nodetype
!= 'NodeFrame':
4151 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4152 props
.to_type
= node
.nodetype
4154 for node
in self
.category
.items(context
):
4155 if isinstance(node
, NodeItemCustom
):
4156 node
.draw(self
, layout
, context
)
4158 props
= layout
.operator(NWSwitchNodeType
.bl_idname
, text
=node
.label
)
4159 props
.to_type
= node
.nodetype
4162 # APPENDAGES TO EXISTING UI
4166 def select_parent_children_buttons(self
, context
):
4167 layout
= self
.layout
4168 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select frame's members (children)").option
= 'CHILD'
4169 layout
.operator(NWSelectParentChildren
.bl_idname
, text
="Select parent frame").option
= 'PARENT'
4172 def attr_nodes_menu_func(self
, context
):
4173 col
= self
.layout
.column(align
=True)
4174 col
.menu("NODE_MT_nw_node_attribute_menu")
4178 def multipleimages_menu_func(self
, context
):
4179 col
= self
.layout
.column(align
=True)
4180 col
.operator(NWAddMultipleImages
.bl_idname
, text
="Multiple Images")
4181 col
.operator(NWAddSequence
.bl_idname
, text
="Image Sequence")
4185 def bgreset_menu_func(self
, context
):
4186 self
.layout
.operator(NWResetBG
.bl_idname
)
4189 def save_viewer_menu_func(self
, context
):
4190 if nw_check(context
):
4191 if context
.space_data
.tree_type
== 'CompositorNodeTree':
4192 if context
.scene
.node_tree
.nodes
.active
:
4193 if context
.scene
.node_tree
.nodes
.active
.type == "VIEWER":
4194 self
.layout
.operator(NWSaveViewer
.bl_idname
, icon
='FILE_IMAGE')
4197 def reset_nodes_button(self
, context
):
4198 node_active
= context
.active_node
4199 node_selected
= context
.selected_nodes
4200 node_ignore
= ["FRAME","REROUTE", "GROUP"]
4202 # Check if active node is in the selection and respective type
4203 if (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type not in node_ignore
:
4204 row
= self
.layout
.row()
4205 row
.operator("node.nw_reset_nodes", text
="Reset Node", icon
="FILE_REFRESH")
4206 self
.layout
.separator()
4208 elif (len(node_selected
) == 1) and node_active
and node_active
.select
and node_active
.type == "FRAME":
4209 row
= self
.layout
.row()
4210 row
.operator("node.nw_reset_nodes", text
="Reset Nodes in Frame", icon
="FILE_REFRESH")
4211 self
.layout
.separator()
4215 # REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
4217 switch_category_menus
= []
4219 # kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
4220 # props entry: (property name, property value)
4223 # NWMergeNodes with Ctrl (AUTO).
4224 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, False,
4225 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4226 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, False,
4227 (('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
4228 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, False,
4229 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4230 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, False,
4231 (('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
4232 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
4233 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4234 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, False,
4235 (('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
4236 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, False,
4237 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4238 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, False,
4239 (('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
4240 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, False,
4241 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4242 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, False,
4243 (('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
4244 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, False, False,
4245 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
4246 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, False, False,
4247 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
4248 (NWMergeNodes
.bl_idname
, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
4249 (('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
4250 # NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
4251 (NWMergeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', True, False, True,
4252 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4253 (NWMergeNodes
.bl_idname
, 'ZERO', 'PRESS', True, False, True,
4254 (('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
4255 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, False, True,
4256 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4257 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, False, True,
4258 (('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
4259 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
4260 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4261 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, False, True,
4262 (('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
4263 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, False, True,
4264 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4265 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, False, True,
4266 (('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
4267 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, False, True,
4268 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4269 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, False, True,
4270 (('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
4271 # NWMergeNodes with Ctrl Shift (MATH)
4272 (NWMergeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', True, True, False,
4273 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4274 (NWMergeNodes
.bl_idname
, 'EQUAL', 'PRESS', True, True, False,
4275 (('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
4276 (NWMergeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
4277 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4278 (NWMergeNodes
.bl_idname
, 'EIGHT', 'PRESS', True, True, False,
4279 (('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
4280 (NWMergeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', True, True, False,
4281 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4282 (NWMergeNodes
.bl_idname
, 'MINUS', 'PRESS', True, True, False,
4283 (('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
4284 (NWMergeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', True, True, False,
4285 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4286 (NWMergeNodes
.bl_idname
, 'SLASH', 'PRESS', True, True, False,
4287 (('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
4288 (NWMergeNodes
.bl_idname
, 'COMMA', 'PRESS', True, True, False,
4289 (('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
4290 (NWMergeNodes
.bl_idname
, 'PERIOD', 'PRESS', True, True, False,
4291 (('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
4292 # BATCH CHANGE NODES
4293 # NWBatchChangeNodes with Alt
4294 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_0', 'PRESS', False, False, True,
4295 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4296 (NWBatchChangeNodes
.bl_idname
, 'ZERO', 'PRESS', False, False, True,
4297 (('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
4298 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_PLUS', 'PRESS', False, False, True,
4299 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4300 (NWBatchChangeNodes
.bl_idname
, 'EQUAL', 'PRESS', False, False, True,
4301 (('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
4302 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
4303 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4304 (NWBatchChangeNodes
.bl_idname
, 'EIGHT', 'PRESS', False, False, True,
4305 (('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
4306 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_MINUS', 'PRESS', False, False, True,
4307 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4308 (NWBatchChangeNodes
.bl_idname
, 'MINUS', 'PRESS', False, False, True,
4309 (('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
4310 (NWBatchChangeNodes
.bl_idname
, 'NUMPAD_SLASH', 'PRESS', False, False, True,
4311 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4312 (NWBatchChangeNodes
.bl_idname
, 'SLASH', 'PRESS', False, False, True,
4313 (('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
4314 (NWBatchChangeNodes
.bl_idname
, 'COMMA', 'PRESS', False, False, True,
4315 (('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
4316 (NWBatchChangeNodes
.bl_idname
, 'PERIOD', 'PRESS', False, False, True,
4317 (('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
4318 (NWBatchChangeNodes
.bl_idname
, 'DOWN_ARROW', 'PRESS', False, False, True,
4319 (('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
4320 (NWBatchChangeNodes
.bl_idname
, 'UP_ARROW', 'PRESS', False, False, True,
4321 (('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
4322 # LINK ACTIVE TO SELECTED
4323 # Don't use names, don't replace links (K)
4324 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, False, False,
4325 (('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
4326 # Don't use names, replace links (Shift K)
4327 (NWLinkActiveToSelected
.bl_idname
, 'K', 'PRESS', False, True, False,
4328 (('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
4329 # Use node name, don't replace links (')
4330 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, False, False,
4331 (('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
4332 # Use node name, replace links (Shift ')
4333 (NWLinkActiveToSelected
.bl_idname
, 'QUOTE', 'PRESS', False, True, False,
4334 (('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
4335 # Don't use names, don't replace links (;)
4336 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, False, False,
4337 (('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
4338 # Don't use names, replace links (')
4339 (NWLinkActiveToSelected
.bl_idname
, 'SEMI_COLON', 'PRESS', False, True, False,
4340 (('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
4342 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
4343 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
4344 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', False, True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
4345 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', False, True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
4346 (NWChangeMixFactor
.bl_idname
, 'LEFT_ARROW', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4347 (NWChangeMixFactor
.bl_idname
, 'RIGHT_ARROW', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4348 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_0', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4349 (NWChangeMixFactor
.bl_idname
, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
4350 (NWChangeMixFactor
.bl_idname
, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
4351 (NWChangeMixFactor
.bl_idname
, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
4352 # CLEAR LABEL (Alt L)
4353 (NWClearLabel
.bl_idname
, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
4354 # MODIFY LABEL (Alt Shift L)
4355 (NWModifyLabels
.bl_idname
, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
4356 # Copy Label from active to selected
4357 (NWCopyLabel
.bl_idname
, 'V', 'PRESS', False, True, False, (('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
4358 # DETACH OUTPUTS (Alt Shift D)
4359 (NWDetachOutputs
.bl_idname
, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
4360 # LINK TO OUTPUT NODE (O)
4361 (NWLinkToOutputNode
.bl_idname
, 'O', 'PRESS', False, False, False, None, "Link to output node"),
4362 # SELECT PARENT/CHILDREN
4364 (NWSelectParentChildren
.bl_idname
, 'RIGHT_BRACKET', 'PRESS', False, False, False, (('option', 'CHILD'),), "Select children"),
4366 (NWSelectParentChildren
.bl_idname
, 'LEFT_BRACKET', 'PRESS', False, False, False, (('option', 'PARENT'),), "Select Parent"),
4368 (NWAddTextureSetup
.bl_idname
, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
4369 # Add Principled BSDF Texture Setup
4370 (NWAddPrincipledSetup
.bl_idname
, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
4372 (NWResetBG
.bl_idname
, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
4374 (NWDeleteUnused
.bl_idname
, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
4376 (NWFrameSelected
.bl_idname
, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
4378 (NWSwapLinks
.bl_idname
, 'S', 'PRESS', False, False, True, None, "Swap Links"),
4380 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', True, True, False, (('run_in_geometry_nodes', False),), "Preview node output"),
4381 (NWPreviewNode
.bl_idname
, 'LEFTMOUSE', 'PRESS', False, True, True, (('run_in_geometry_nodes', True),), "Preview node output"),
4383 (NWReloadImages
.bl_idname
, 'R', 'PRESS', False, False, True, None, "Reload images"),
4385 (NWLazyMix
.bl_idname
, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
4387 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
4388 # Lazy Connect with Menu
4389 (NWLazyConnect
.bl_idname
, 'RIGHTMOUSE', 'PRESS', False, True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
4390 # Viewer Tile Center
4391 (NWViewerFocus
.bl_idname
, 'LEFTMOUSE', 'DOUBLE_CLICK', False, False, False, None, "Set Viewers Tile Center"),
4393 (NWAlignNodes
.bl_idname
, 'EQUAL', 'PRESS', False, True, False, None, "Align selected nodes neatly in a row/column"),
4394 # Reset Nodes (Back Space)
4395 (NWResetNodes
.bl_idname
, 'BACK_SPACE', 'PRESS', False, False, False, None, "Revert node back to default state, but keep connections"),
4397 ('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', NodeWranglerMenu
.bl_idname
),), "Node Wrangler menu"),
4398 ('wm.call_menu', 'SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4399 ('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False, (('name', NWAddReroutesMenu
.bl_idname
),), "Add Reroutes menu"),
4400 ('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False, (('name', NWLinkActiveToSelectedMenu
.bl_idname
),), "Link active to selected (menu)"),
4401 ('wm.call_menu', 'C', 'PRESS', False, True, False, (('name', NWCopyToSelectedMenu
.bl_idname
),), "Copy to selected (menu)"),
4402 ('wm.call_menu', 'S', 'PRESS', False, True, False, (('name', NWSwitchNodeTypeMenu
.bl_idname
),), "Switch node type menu"),
4407 NWPrincipledPreferences
,
4427 NWAddPrincipledSetup
,
4429 NWLinkActiveToSelected
,
4431 NWSelectParentChildren
,
4437 NWAddMultipleImages
,
4445 NWMergeGeometryMenu
,
4447 NWConnectionListOutputs
,
4448 NWConnectionListInputs
,
4450 NWBatchChangeNodesMenu
,
4451 NWBatchChangeBlendTypeMenu
,
4452 NWBatchChangeOperationMenu
,
4453 NWCopyToSelectedMenu
,
4456 NWLinkActiveToSelectedMenu
,
4458 NWLinkUseNodeNameMenu
,
4459 NWLinkUseOutputsNamesMenu
,
4461 NWSwitchNodeTypeMenu
,
4465 from bpy
.utils
import register_class
4468 bpy
.types
.Scene
.NWBusyDrawing
= StringProperty(
4469 name
="Busy Drawing!",
4471 description
="An internal property used to store only the first mouse position")
4472 bpy
.types
.Scene
.NWLazySource
= StringProperty(
4473 name
="Lazy Source!",
4475 description
="An internal property used to store the first node in a Lazy Connect operation")
4476 bpy
.types
.Scene
.NWLazyTarget
= StringProperty(
4477 name
="Lazy Target!",
4479 description
="An internal property used to store the last node in a Lazy Connect operation")
4480 bpy
.types
.Scene
.NWSourceSocket
= IntProperty(
4481 name
="Source Socket!",
4483 description
="An internal property used to store the source socket in a Lazy Connect operation")
4484 bpy
.types
.NodeSocketInterface
.NWViewerSocket
= BoolProperty(
4487 description
="An internal property used to determine if a socket is generated by the addon"
4494 addon_keymaps
.clear()
4495 kc
= bpy
.context
.window_manager
.keyconfigs
.addon
4497 km
= kc
.keymaps
.new(name
='Node Editor', space_type
="NODE_EDITOR")
4498 for (identifier
, key
, action
, CTRL
, SHIFT
, ALT
, props
, nicename
) in kmi_defs
:
4499 kmi
= km
.keymap_items
.new(identifier
, key
, action
, ctrl
=CTRL
, shift
=SHIFT
, alt
=ALT
)
4501 for prop
, value
in props
:
4502 setattr(kmi
.properties
, prop
, value
)
4503 addon_keymaps
.append((km
, kmi
))
4506 bpy
.types
.NODE_MT_select
.append(select_parent_children_buttons
)
4507 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.prepend(attr_nodes_menu_func
)
4508 bpy
.types
.NODE_PT_backdrop
.append(bgreset_menu_func
)
4509 bpy
.types
.NODE_PT_active_node_generic
.append(save_viewer_menu_func
)
4510 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.prepend(multipleimages_menu_func
)
4511 bpy
.types
.NODE_MT_category_CMP_INPUT
.prepend(multipleimages_menu_func
)
4512 bpy
.types
.NODE_PT_active_node_generic
.prepend(reset_nodes_button
)
4513 bpy
.types
.NODE_MT_node
.prepend(reset_nodes_button
)
4516 switch_category_menus
.clear()
4517 for cat
in node_categories_iter(None):
4518 if cat
.name
not in ['Group', 'Script']:
4519 idname
= f
"NODE_MT_nw_switch_{cat.identifier}_submenu"
4520 switch_category_type
= type(idname
, (bpy
.types
.Menu
,), {
4521 "bl_space_type": 'NODE_EDITOR',
4522 "bl_label": cat
.name
,
4525 "draw": draw_switch_category_submenu
,
4528 switch_category_menus
.append(switch_category_type
)
4530 bpy
.utils
.register_class(switch_category_type
)
4534 from bpy
.utils
import unregister_class
4537 del bpy
.types
.Scene
.NWBusyDrawing
4538 del bpy
.types
.Scene
.NWLazySource
4539 del bpy
.types
.Scene
.NWLazyTarget
4540 del bpy
.types
.Scene
.NWSourceSocket
4541 del bpy
.types
.NodeSocketInterface
.NWViewerSocket
4543 for cat_types
in switch_category_menus
:
4544 bpy
.utils
.unregister_class(cat_types
)
4545 switch_category_menus
.clear()
4548 for km
, kmi
in addon_keymaps
:
4549 km
.keymap_items
.remove(kmi
)
4550 addon_keymaps
.clear()
4553 bpy
.types
.NODE_MT_select
.remove(select_parent_children_buttons
)
4554 bpy
.types
.NODE_MT_category_SH_NEW_INPUT
.remove(attr_nodes_menu_func
)
4555 bpy
.types
.NODE_PT_backdrop
.remove(bgreset_menu_func
)
4556 bpy
.types
.NODE_PT_active_node_generic
.remove(save_viewer_menu_func
)
4557 bpy
.types
.NODE_MT_category_SH_NEW_TEXTURE
.remove(multipleimages_menu_func
)
4558 bpy
.types
.NODE_MT_category_CMP_INPUT
.remove(multipleimages_menu_func
)
4559 bpy
.types
.NODE_PT_active_node_generic
.remove(reset_nodes_button
)
4560 bpy
.types
.NODE_MT_node
.remove(reset_nodes_button
)
4563 unregister_class(cls
)
4565 if __name__
== "__main__":