Newer
Older
is_main_tree = True
if active:
is_main_tree = context_active == active
if not is_main_tree: # if group is currently edited
tree = active.node_tree
nodes = tree.nodes
links = tree.links
return nodes, links
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
# Addon prefs
class NWNodeWrangler(bpy.types.AddonPreferences):
bl_idname = __name__
merge_hide = EnumProperty(
name="Hide Mix nodes",
items=(
("ALWAYS", "Always", "Always collapse the new merge nodes"),
("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
("NEVER", "Never", "Never collapse the new merge nodes")
),
default='NON_SHADER',
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy whether to collapse them or show the full node with options expanded")
merge_position = EnumProperty(
name="Mix Node Position",
items=(
("CENTER", "Center", "Place the Mix node between the two nodes"),
("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
),
default='CENTER',
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specifiy the position of the new nodes")
show_hotkey_list = BoolProperty(
name="Show Hotkey List",
default=False,
description="Expand this box into a list of all the hotkeys for functions in this addon"
)
hotkey_list_filter = StringProperty(
name=" Filter by Name",
default="",
description="Show only hotkeys that have this text in their name"
)
def draw(self, context):
layout = self.layout
col = layout.column()
col.prop(self, "merge_position")
col.prop(self, "merge_hide")
box = col.box()
col = box.column(align=True)
hotkey_button_name = "Show Hotkey List"
if self.show_hotkey_list:
hotkey_button_name = "Hide Hotkey List"
col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
if self.show_hotkey_list:
col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
col.separator()
for hotkey in kmi_defs:
if self.hotkey_list_filter.lower() in hotkey_name.lower():
row = col.row(align=True)
row.label(hotkey_name)
keystr = nice_hotkey_name(hotkey[1])
if hotkey[4]:
keystr = "Alt " + keystr
keystr = "Ctrl " + keystr
row.label(keystr)
def nw_check(context):
space = context.space_data
valid = False
if space.type == 'NODE_EDITOR' and space.node_tree is not None:
class NWBase:
@classmethod
def poll(cls, context):
return nw_check(context)
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
# OPERATORS
class NWLazyMix(Operator, NWBase):
"""Add a Mix RGB/Shader node by interactively drawing lines between nodes"""
bl_idname = "node.nw_lazy_mix"
bl_label = "Mix Nodes"
bl_options = {'REGISTER', 'UNDO'}
def modal(self, context, event):
context.area.tag_redraw()
nodes, links = get_nodes_links(context)
cont = True
start_pos = [event.mouse_region_x, event.mouse_region_y]
node1 = None
if not context.scene.NWBusyDrawing:
node1 = node_at_pos(nodes, context, event)
if node1:
context.scene.NWBusyDrawing = node1.name
else:
if context.scene.NWBusyDrawing != 'STOP':
node1 = nodes[context.scene.NWBusyDrawing]
context.scene.NWLazySource = node1.name
context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
if event.type == 'MOUSEMOVE':
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
elif event.type == 'RIGHTMOUSE':
end_pos = [event.mouse_region_x, event.mouse_region_y]
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
node2 = None
node2 = node_at_pos(nodes, context, event)
if node2:
context.scene.NWBusyDrawing = node2.name
if node1 == node2:
cont = False
if cont:
if node1 and node2:
for node in nodes:
node.select = False
node1.select = True
node2.select = True
bpy.ops.node.nw_merge_nodes(mode="MIX", merge_type="AUTO")
context.scene.NWBusyDrawing = ""
return {'FINISHED'}
elif event.type == 'ESC':
print('cancelled')
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'NODE_EDITOR':
# the arguments we pass the the callback
args = (self, context, 'MIX')
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
self.mouse_path = []
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
class NWLazyConnect(Operator, NWBase):
"""Connect two nodes without clicking a specific socket (automatically determined"""
bl_idname = "node.nw_lazy_connect"
bl_label = "Lazy Connect"
bl_options = {'REGISTER', 'UNDO'}
with_menu = BoolProperty()
def modal(self, context, event):
context.area.tag_redraw()
nodes, links = get_nodes_links(context)
cont = True
start_pos = [event.mouse_region_x, event.mouse_region_y]
node1 = None
if not context.scene.NWBusyDrawing:
node1 = node_at_pos(nodes, context, event)
if node1:
context.scene.NWBusyDrawing = node1.name
else:
if context.scene.NWBusyDrawing != 'STOP':
node1 = nodes[context.scene.NWBusyDrawing]
context.scene.NWLazySource = node1.name
context.scene.NWLazyTarget = node_at_pos(nodes, context, event).name
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
if event.type == 'MOUSEMOVE':
self.mouse_path.append((event.mouse_region_x, event.mouse_region_y))
elif event.type == 'RIGHTMOUSE':
end_pos = [event.mouse_region_x, event.mouse_region_y]
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
node2 = None
node2 = node_at_pos(nodes, context, event)
if node2:
context.scene.NWBusyDrawing = node2.name
if node1 == node2:
cont = False
link_success = False
if cont:
if node1 and node2:
original_sel = []
original_unsel = []
for node in nodes:
if node.select == True:
node.select = False
original_sel.append(node)
else:
original_unsel.append(node)
node1.select = True
node2.select = True
#link_success = autolink(node1, node2, links)
if self.with_menu:
if len(node1.outputs) > 1 and node2.inputs:
bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListOutputs.bl_idname)
elif len(node1.outputs) == 1:
bpy.ops.node.nw_call_inputs_menu(from_socket=0)
else:
link_success = autolink(node1, node2, links)
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
for node in original_sel:
node.select = True
for node in original_unsel:
node.select = False
if link_success:
hack_force_update(context, nodes)
context.scene.NWBusyDrawing = ""
return {'FINISHED'}
elif event.type == 'ESC':
bpy.types.SpaceNodeEditor.draw_handler_remove(self._handle, 'WINDOW')
return {'CANCELLED'}
return {'RUNNING_MODAL'}
def invoke(self, context, event):
if context.area.type == 'NODE_EDITOR':
nodes, links = get_nodes_links(context)
node = node_at_pos(nodes, context, event)
if node:
context.scene.NWBusyDrawing = node.name
# the arguments we pass the the callback
mode = "LINK"
if self.with_menu:
mode = "LINKMENU"
args = (self, context, mode)
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
self._handle = bpy.types.SpaceNodeEditor.draw_handler_add(draw_callback_nodeoutline, args, 'WINDOW', 'POST_PIXEL')
self.mouse_path = []
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
else:
self.report({'WARNING'}, "View3D not found, cannot run operator")
return {'CANCELLED'}
class NWDeleteUnused(Operator, NWBase):
"""Delete all nodes whose output is not used"""
bl_idname = 'node.nw_del_unused'
bl_label = 'Delete Unused Nodes'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
if context.space_data.node_tree.nodes:
valid = True
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
end_types = 'OUTPUT_MATERIAL', 'OUTPUT', 'VIEWER', 'COMPOSITE', \
'SPLITVIEWER', 'OUTPUT_FILE', 'LEVELS', 'OUTPUT_LAMP', \
'OUTPUT_WORLD', 'GROUP', 'GROUP_INPUT', 'GROUP_OUTPUT'
# Store selection
selection = []
for node in nodes:
if node.select == True:
selection.append(node.name)
deleted_nodes = []
temp_deleted_nodes = []
del_unused_iterations = len(nodes)
for it in range(0, del_unused_iterations):
temp_deleted_nodes = list(deleted_nodes) # keep record of last iteration
for node in nodes:
node.select = False
for node in nodes:
if is_end_node(node) and not node.type in end_types and node.type != 'FRAME':
node.select = True
deleted_nodes.append(node.name)
bpy.ops.node.delete()
if temp_deleted_nodes == deleted_nodes: # stop iterations when there are no more nodes to be deleted
break
# get unique list of deleted nodes (iterations would count the same node more than once)
deleted_nodes = list(set(deleted_nodes))
for n in deleted_nodes:
self.report({'INFO'}, "Node " + n + " deleted")
num_deleted = len(deleted_nodes)
n = ' node'
if num_deleted > 1:
n += 's'
if num_deleted:
self.report({'INFO'}, "Deleted " + str(num_deleted) + n)
else:
self.report({'INFO'}, "Nothing deleted")
# Restore selection
nodes, links = get_nodes_links(context)
for node in nodes:
if node.name in selection:
node.select = True
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
class NWSwapLinks(Operator, NWBase):
"""Swap the output connections of the two selected nodes, or two similar inputs of a single node"""
bl_idname = 'node.nw_swap_links'
bl_label = 'Swap Links'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
if context.selected_nodes:
valid = len(context.selected_nodes) <= 2
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
selected_nodes = context.selected_nodes
n1 = selected_nodes[0]
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
# Swap outputs
if len(selected_nodes) == 2:
n2 = selected_nodes[1]
if n1.outputs and n2.outputs:
n1_outputs = []
n2_outputs = []
out_index = 0
for output in n1.outputs:
if output.links:
for link in output.links:
n1_outputs.append([out_index, link.to_socket])
links.remove(link)
out_index += 1
out_index = 0
for output in n2.outputs:
if output.links:
for link in output.links:
n2_outputs.append([out_index, link.to_socket])
links.remove(link)
out_index += 1
for connection in n1_outputs:
try:
links.new(n2.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
for connection in n2_outputs:
try:
links.new(n1.outputs[connection[0]], connection[1])
except:
self.report({'WARNING'}, "Some connections have been lost due to differing numbers of output sockets")
else:
if n1.outputs or n2.outputs:
self.report({'WARNING'}, "One of the nodes has no outputs!")
else:
self.report({'WARNING'}, "Neither of the nodes have outputs!")
# Swap Inputs
elif len(selected_nodes) == 1:
if n1.inputs:
types = []
i=0
for i1 in n1.inputs:
if i1.is_linked:
similar_types = 0
for i2 in n1.inputs:
if i1.type == i2.type and i2.is_linked:
similar_types += 1
types.append ([i1, similar_types, i])
i += 1
types.sort(key=lambda k: k[1], reverse=True)
if types:
t = types[0]
if t[1] == 2:
for i2 in n1.inputs:
if t[0].type == i2.type == t[0].type and t[0] != i2 and i2.is_linked:
pair = [t[0], i2]
i1f = pair[0].links[0].from_socket
i1t = pair[0].links[0].to_socket
i2f = pair[1].links[0].from_socket
i2t = pair[1].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
if t[1] == 1:
if len(types) == 1:
fs = t[0].links[0].from_socket
i = t[2]
links.remove(t[0].links[0])
if i+1 == len(n1.inputs):
i = -1
i += 1
while n1.inputs[i].is_linked:
i += 1
links.new(fs, n1.inputs[i])
elif len(types) == 2:
i1f = types[0][0].links[0].from_socket
i1t = types[0][0].links[0].to_socket
i2f = types[1][0].links[0].from_socket
i2t = types[1][0].links[0].to_socket
links.new(i1f, i2t)
links.new(i2f, i1t)
else:
self.report({'WARNING'}, "This node has no input connections to swap!")
else:
self.report({'WARNING'}, "This node has no inputs to swap!")
hack_force_update(context, nodes)
return {'FINISHED'}
class NWResetBG(Operator, NWBase):
"""Reset the zoom and position of the background image"""
bl_idname = 'node.nw_bg_reset'
bl_label = 'Reset Backdrop'
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
snode = context.space_data
valid = snode.tree_type == 'CompositorNodeTree'
return valid
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
def execute(self, context):
context.space_data.backdrop_zoom = 1
context.space_data.backdrop_x = 0
context.space_data.backdrop_y = 0
return {'FINISHED'}
class NWAddAttrNode(Operator, NWBase):
"""Add an Attribute node with this name"""
bl_idname = 'node.nw_add_attr_node'
bl_label = 'Add UV map'
attr_name = StringProperty()
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type="ShaderNodeAttribute")
nodes, links = get_nodes_links(context)
nodes.active.attribute_name = self.attr_name
return {'FINISHED'}
class NWEmissionViewer(Operator, NWBase):
bl_idname = "node.nw_emission_viewer"
bl_label = "Emission Viewer"
bl_description = "Connect active node to Emission Shader for shadeless previews"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
is_cycles = context.scene.render.engine == 'CYCLES'
if nw_check(context):
space = context.space_data
if space.tree_type == 'ShaderNodeTree' and is_cycles:
if context.active_node:
if context.active_node.type != "OUTPUT_MATERIAL" or context.active_node.type != "OUTPUT_WORLD":
return True
else:
return True
return False
def invoke(self, context, event):
space = context.space_data
shader_type = space.shader_type
if shader_type == 'OBJECT':
if space.id not in [lamp for lamp in bpy.data.lamps]: # cannot use bpy.data.lamps directly as iterable
shader_output_type = "OUTPUT_MATERIAL"
shader_output_ident = "ShaderNodeOutputMaterial"
shader_viewer_ident = "ShaderNodeEmission"
else:
shader_output_type = "OUTPUT_LAMP"
shader_output_ident = "ShaderNodeOutputLamp"
shader_viewer_ident = "ShaderNodeEmission"
elif shader_type == 'WORLD':
shader_output_type = "OUTPUT_WORLD"
shader_output_ident = "ShaderNodeOutputWorld"
shader_viewer_ident = "ShaderNodeBackground"
shader_types = [x[1] for x in shaders_shader_nodes_props]
mlocx = event.mouse_region_x
mlocy = event.mouse_region_y
select_node = bpy.ops.node.select(mouse_x=mlocx, mouse_y=mlocy, extend=False)
if 'FINISHED' in select_node: # only run if mouse click is on a node
nodes, links = get_nodes_links(context)
in_group = context.active_node != space.node_tree.nodes.active
active = nodes.active
output_types = [x[1] for x in shaders_output_nodes_props]
if active:
if (active.name != "Emission Viewer") and (active.type not in output_types) and not in_group:
for out in active.outputs:
if not out.hide:
valid = True
break
# get material_output node, store selection, deselect all
materialout = None # placeholder node
for node in nodes:
if node.type == shader_output_type:
materialout = node
if node.select:
selection.append(node.name)
node.select = False
if not materialout:
sorted_by_xloc = (sorted(nodes, key=lambda x: x.location.x))
max_xloc_node = sorted_by_xloc[-1]
if max_xloc_node.name == 'Emission Viewer':
max_xloc_node = sorted_by_xloc[-2]
sum_yloc = 0
for node in nodes:
sum_yloc += node.location.y
new_locx = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
new_locy = sum_yloc / len(nodes)
materialout = nodes.new(shader_output_ident)
materialout.location.x = new_locx
materialout.location.y = new_locy
materialout.select = False
# Analyze outputs, add "Emission Viewer" if needed, make links
out_i = None
valid_outputs = []
for i, out in enumerate(active.outputs):
if not out.hide:
valid_outputs.append(i)
if valid_outputs:
out_i = valid_outputs[0] # Start index of node's outputs
for i, valid_i in enumerate(valid_outputs):
for out_link in active.outputs[valid_i].links:
linked_to_out = False
if "Emission Viewer" in out_link.to_node.name or out_link.to_node == materialout:
linked_to_out = True
if linked_to_out:
if i < len(valid_outputs) - 1:
out_i = valid_outputs[i + 1]
else:
out_i = valid_outputs[0]
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
make_links = [] # store sockets for new links
if active.outputs:
# If output type not 'SHADER' - "Emission Viewer" needed
if active.outputs[out_i].type != 'SHADER':
# get Emission Viewer node
emission_exists = False
emission_placeholder = nodes[0]
for node in nodes:
if "Emission Viewer" in node.name:
emission_exists = True
emission_placeholder = node
if not emission_exists:
emission = nodes.new(shader_viewer_ident)
emission.hide = True
emission.location = [materialout.location.x, (materialout.location.y + 40)]
emission.label = "Viewer"
emission.name = "Emission Viewer"
emission.use_custom_color = True
emission.color = (0.6, 0.5, 0.4)
emission.select = False
else:
emission = emission_placeholder
make_links.append((active.outputs[out_i], emission.inputs[0]))
make_links.append((emission.outputs[0], materialout.inputs[0]))
else:
# Output type is 'SHADER', no Viewer needed. Delete Viewer if exists.
make_links.append((active.outputs[out_i], materialout.inputs[1 if active.outputs[out_i].name == "Volume" else 0]))
for node in nodes:
if node.name == 'Emission Viewer':
node.select = True
bpy.ops.node.delete()
for li_from, li_to in make_links:
links.new(li_from, li_to)
# Restore selection
nodes.active = active
for node in nodes:
if node.name in selection:
node.select = True
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
return {'FINISHED'}
else:
return {'CANCELLED'}
class NWFrameSelected(Operator, NWBase):
bl_idname = "node.nw_frame_selected"
bl_label = "Frame Selected"
bl_description = "Add a frame node and parent the selected nodes to it"
bl_options = {'REGISTER', 'UNDO'}
label_prop = StringProperty(name='Label', default=' ', description='The visual name of the frame node')
color_prop = FloatVectorProperty(name="Color", description="The color of the frame node", default=(0.6, 0.6, 0.6),
min=0, max=1, step=1, precision=3, subtype='COLOR_GAMMA', size=3)
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = []
for node in nodes:
if node.select == True:
selected.append(node)
bpy.ops.node.add_node(type='NodeFrame')
frm = nodes.active
frm.label = self.label_prop
frm.use_custom_color = True
frm.color = self.color_prop
for node in selected:
node.parent = frm
return {'FINISHED'}
class NWReloadImages(Operator, NWBase):
bl_idname = "node.nw_reload_images"
bl_label = "Reload Images"
bl_description = "Update all the image nodes to match their files on disk"
def execute(self, context):
nodes, links = get_nodes_links(context)
image_types = ["IMAGE", "TEX_IMAGE", "TEX_ENVIRONMENT", "TEXTURE"]
num_reloaded = 0
for node in nodes:
if node.type in image_types:
if node.type == "TEXTURE":
if node.texture: # node has texture assigned
if node.texture.type in ['IMAGE', 'ENVIRONMENT_MAP']:
if node.texture.image: # texture has image assigned
node.texture.image.reload()
num_reloaded += 1
else:
if node.image:
node.image.reload()
num_reloaded += 1
if num_reloaded:
self.report({'INFO'}, "Reloaded images")
print("Reloaded " + str(num_reloaded) + " images")
hack_force_update(context, nodes)
return {'FINISHED'}
else:
self.report({'WARNING'}, "No images found to reload in this node tree")
return {'CANCELLED'}
class NWSwitchNodeType(Operator, NWBase):
"""Switch type of selected nodes """
bl_idname = "node.nw_swtch_node_type"
bl_label = "Switch Node Type"
bl_options = {'REGISTER', 'UNDO'}
to_type = EnumProperty(
name="Switch to type",
items=list(shaders_input_nodes_props) +
list(shaders_output_nodes_props) +
list(shaders_shader_nodes_props) +
list(shaders_texture_nodes_props) +
list(shaders_color_nodes_props) +
list(shaders_vector_nodes_props) +
list(shaders_converter_nodes_props) +
list(shaders_layout_nodes_props) +
list(compo_input_nodes_props) +
list(compo_output_nodes_props) +
list(compo_color_nodes_props) +
list(compo_converter_nodes_props) +
list(compo_filter_nodes_props) +
list(compo_vector_nodes_props) +
list(compo_matte_nodes_props) +
list(compo_distort_nodes_props) +
list(compo_layout_nodes_props) +
list(blender_mat_input_nodes_props) +
list(blender_mat_output_nodes_props) +
list(blender_mat_color_nodes_props) +
list(blender_mat_vector_nodes_props) +
list(blender_mat_converter_nodes_props) +
list(blender_mat_layout_nodes_props) +
list(texture_input_nodes_props) +
list(texture_output_nodes_props) +
list(texture_color_nodes_props) +
list(texture_pattern_nodes_props) +
list(texture_textures_nodes_props) +
list(texture_converter_nodes_props) +
list(texture_distort_nodes_props) +
list(texture_layout_nodes_props)
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
)
def execute(self, context):
nodes, links = get_nodes_links(context)
to_type = self.to_type
# Those types of nodes will not swap.
src_excludes = ('NodeFrame')
# Those attributes of nodes will be copied if possible
attrs_to_pass = ('color', 'hide', 'label', 'mute', 'parent',
'show_options', 'show_preview', 'show_texture',
'use_alpha', 'use_clamp', 'use_custom_color', 'location'
)
selected = [n for n in nodes if n.select]
reselect = []
for node in [n for n in selected if
n.rna_type.identifier not in src_excludes and
n.rna_type.identifier != to_type]:
new_node = nodes.new(to_type)
for attr in attrs_to_pass:
if hasattr(node, attr) and hasattr(new_node, attr):
setattr(new_node, attr, getattr(node, attr))
# set image datablock of dst to image of src
if hasattr(node, 'image') and hasattr(new_node, 'image'):
if node.image:
new_node.image = node.image
# Special cases
if new_node.type == 'SWITCH':
new_node.hide = True
# Dictionaries: src_sockets and dst_sockets:
# 'INPUTS': input sockets ordered by type (entry 'MAIN' main type of inputs).
# 'OUTPUTS': output sockets ordered by type (entry 'MAIN' main type of outputs).
# in 'INPUTS' and 'OUTPUTS':
# 'SHADER', 'RGBA', 'VECTOR', 'VALUE' - sockets of those types.
# socket entry:
# (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
src_sockets = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
}
dst_sockets = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE': [], 'MAIN': None},
}
types_order_one = 'SHADER', 'RGBA', 'VECTOR', 'VALUE'
types_order_two = 'SHADER', 'VECTOR', 'RGBA', 'VALUE'
# check src node to set src_sockets values and dst node to set dst_sockets dict values
for sockets, nd in ((src_sockets, node), (dst_sockets, new_node)):
# Check node's inputs and outputs and fill proper entries in "sockets" dict
for in_out, in_out_name in ((nd.inputs, 'INPUTS'), (nd.outputs, 'OUTPUTS')):
# enumerate in inputs, then in outputs
# find name, default value and links of socket
for i, socket in enumerate(in_out):
the_name = socket.name
dval = None
# Not every socket, especially in outputs has "default_value"
if hasattr(socket, 'default_value'):
dval = socket.default_value
socket_links = []
for lnk in socket.links:
socket_links.append(lnk)
# check type of socket to fill proper keys.
for the_type in types_order_one:
if socket.type == the_type:
# create values for sockets['INPUTS'][the_type] and sockets['OUTPUTS'][the_type]
# entry structure: (index_in_type, socket_index, socket_name, socket_default_value, socket_links)
sockets[in_out_name][the_type].append((len(sockets[in_out_name][the_type]), i, the_name, dval, socket_links))
# Check which of the types in inputs/outputs is considered to be "main".
# Set values of sockets['INPUTS']['MAIN'] and sockets['OUTPUTS']['MAIN']
for type_check in types_order_one:
if sockets[in_out_name][type_check]:
sockets[in_out_name]['MAIN'] = type_check
break
matches = {
'INPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
'OUTPUTS': {'SHADER': [], 'RGBA': [], 'VECTOR': [], 'VALUE_NAME': [], 'VALUE': [], 'MAIN': []},
}
for inout, soctype in (
('INPUTS', 'MAIN',),
('INPUTS', 'SHADER',),
('INPUTS', 'RGBA',),
('INPUTS', 'VECTOR',),
('INPUTS', 'VALUE',),
('OUTPUTS', 'MAIN',),
('OUTPUTS', 'SHADER',),
('OUTPUTS', 'RGBA',),
('OUTPUTS', 'VECTOR',),
('OUTPUTS', 'VALUE',),
):
if src_sockets[inout][soctype] and dst_sockets[inout][soctype]:
if soctype == 'MAIN':
sc = src_sockets[inout][src_sockets[inout]['MAIN']]
dt = dst_sockets[inout][dst_sockets[inout]['MAIN']]
else:
sc = src_sockets[inout][soctype]
dt = dst_sockets[inout][soctype]
# start with 'dt' to determine number of possibilities.
for i, soc in enumerate(dt):
# if src main has enough entries - match them with dst main sockets by indexes.
if len(sc) > i:
matches[inout][soctype].append(((sc[i][1], sc[i][3]), (soc[1], soc[3])))
# add 'VALUE_NAME' criterion to inputs.
if inout == 'INPUTS' and soctype == 'VALUE':
for s in sc:
if s[2] == soc[2]: # if names match
# append src (index, dval), dst (index, dval)
matches['INPUTS']['VALUE_NAME'].append(((s[1], s[3]), (soc[1], soc[3])))
# When src ['INPUTS']['MAIN'] is 'VECTOR' replace 'MAIN' with matches VECTOR if possible.
# This creates better links when relinking textures.
if src_sockets['INPUTS']['MAIN'] == 'VECTOR' and matches['INPUTS']['VECTOR']:
matches['INPUTS']['MAIN'] = matches['INPUTS']['VECTOR']
# Pass default values and RELINK:
for tp in ('MAIN', 'SHADER', 'RGBA', 'VECTOR', 'VALUE_NAME', 'VALUE'):
# INPUTS: Base on matches in proper order.
for (src_i, src_dval), (dst_i, dst_dval) in matches['INPUTS'][tp]:
# pass dvals
if src_dval and dst_dval and tp in {'RGBA', 'VALUE_NAME'}:
new_node.inputs[dst_i].default_value = src_dval
# Special case: switch to math
if node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
new_node.type == 'MATH' and\
tp == 'MAIN':
new_dst_dval = max(src_dval[0], src_dval[1], src_dval[2])
new_node.inputs[dst_i].default_value = new_dst_dval
if node.type == 'MIX_RGB':
if node.blend_type in [o[0] for o in operations]:
new_node.operation = node.blend_type
# Special case: switch from math to some types
if node.type == 'MATH' and\
new_node.type in {'MIX_RGB', 'ALPHAOVER', 'ZCOMBINE'} and\
tp == 'MAIN':
for i in range(3):
new_node.inputs[dst_i].default_value[i] = src_dval
if new_node.type == 'MIX_RGB':
if node.operation in [t[0] for t in blend_types]:
new_node.blend_type = node.operation
# Set Fac of MIX_RGB to 1.0
new_node.inputs[0].default_value = 1.0
# make link only when dst matching input is not linked already.
if node.inputs[src_i].links and not new_node.inputs[dst_i].links:
in_src_link = node.inputs[src_i].links[0]
in_dst_socket = new_node.inputs[dst_i]
links.new(in_src_link.from_socket, in_dst_socket)
links.remove(in_src_link)
# OUTPUTS: Base on matches in proper order.
for (src_i, src_dval), (dst_i, dst_dval) in matches['OUTPUTS'][tp]:
for out_src_link in node.outputs[src_i].links:
out_dst_socket = new_node.outputs[dst_i]
links.new(out_dst_socket, out_src_link.to_socket)
# relink rest inputs if possible, no criteria
for src_inp in node.inputs:
for dst_inp in new_node.inputs:
if src_inp.links and not dst_inp.links:
src_link = src_inp.links[0]
links.new(src_link.from_socket, dst_inp)
links.remove(src_link)
# relink rest outputs if possible, base on node kind if any left.
for src_o in node.outputs:
for out_src_link in src_o.links:
for dst_o in new_node.outputs:
if src_o.type == dst_o.type:
links.new(dst_o, out_src_link.to_socket)
# relink rest outputs no criteria if any left. Link all from first output.
for src_o in node.outputs:
for out_src_link in src_o.links:
if new_node.outputs:
links.new(new_node.outputs[0], out_src_link.to_socket)
nodes.remove(node)
return {'FINISHED'}
class NWMergeNodes(Operator, NWBase):
bl_idname = "node.nw_merge_nodes"
bl_label = "Merge Nodes"
bl_description = "Merge Selected Nodes"
bl_options = {'REGISTER', 'UNDO'}
mode = EnumProperty(
name="mode",
description="All possible blend types and math operations",
items=blend_types + [op for op in operations if op not in blend_types],
)
merge_type = EnumProperty(
name="merge type",
description="Type of Merge to be used",
items=(
('AUTO', 'Auto', 'Automatic Output Type Detection'),
('SHADER', 'Shader', 'Merge using ADD or MIX Shader'),
('MIX', 'Mix Node', 'Merge using Mix Nodes'),
('MATH', 'Math Node', 'Merge using Math Nodes'),
('ZCOMBINE', 'Z-Combine Node', 'Merge using Z-Combine Nodes'),
('ALPHAOVER', 'Alpha Over Node', 'Merge using Alpha Over Nodes'),
def execute(self, context):
settings = context.user_preferences.addons[__name__].preferences
merge_hide = settings.merge_hide
merge_position = settings.merge_position # 'center' or 'bottom'
do_hide = False
do_hide_shader = False
if merge_hide == 'ALWAYS':
do_hide = True
do_hide_shader = True
elif merge_hide == 'NON_SHADER':
do_hide = True
tree_type = context.space_data.node_tree.type
if tree_type == 'COMPOSITING':
node_type = 'CompositorNode'
elif tree_type == 'SHADER':
node_type = 'ShaderNode'
elif tree_type == 'TEXTURE':
node_type = 'TextureNode'
nodes, links = get_nodes_links(context)
mode = self.mode
merge_type = self.merge_type
# Prevent trying to add Z-Combine in not 'COMPOSITING' node tree.
# 'ZCOMBINE' works only if mode == 'MIX'
# Setting mode to None prevents trying to add 'ZCOMBINE' node.
if (merge_type == 'ZCOMBINE' or merge_type == 'ALPHAOVER') and tree_type != 'COMPOSITING':
merge_type = 'MIX'
mode = 'MIX'
selected_mix = [] # entry = [index, loc]
selected_shader = [] # entry = [index, loc]
selected_math = [] # entry = [index, loc]
selected_z = [] # entry = [index, loc]
selected_alphaover = [] # entry = [index, loc]
for i, node in enumerate(nodes):
if node.select and node.outputs:
if merge_type == 'AUTO':
for (type, types_list, dst) in (
('SHADER', ('MIX', 'ADD'), selected_shader),
('RGBA', [t[0] for t in blend_types], selected_mix),
('VALUE', [t[0] for t in operations], selected_math),
output_type = node.outputs[0].type
valid_mode = mode in types_list
# When mode is 'MIX' use mix node for both 'RGBA' and 'VALUE' output types.
# Cheat that output type is 'RGBA',
# and that 'MIX' exists in math operations list.
# This way when selected_mix list is analyzed:
# Node data will be appended even though it doesn't meet requirements.
if output_type != 'SHADER' and mode == 'MIX':
output_type = 'RGBA'
valid_mode = True
if output_type == type and valid_mode:
dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
else:
for (type, types_list, dst) in (
('SHADER', ('MIX', 'ADD'), selected_shader),
('MIX', [t[0] for t in blend_types], selected_mix),
('MATH', [t[0] for t in operations], selected_math),
('ZCOMBINE', ('MIX', ), selected_z),
('ALPHAOVER', ('MIX', ), selected_alphaover),
if merge_type == type and mode in types_list:
dst.append([i, node.location.x, node.location.y, node.dimensions.x, node.hide])
# When nodes with output kinds 'RGBA' and 'VALUE' are selected at the same time
# use only 'Mix' nodes for merging.
# For that we add selected_math list to selected_mix list and clear selected_math.
if selected_mix and selected_math and merge_type == 'AUTO':
selected_mix += selected_math
selected_math = []
for nodes_list in [selected_mix, selected_shader, selected_math, selected_z, selected_alphaover]:
if nodes_list:
count_before = len(nodes)