Newer
Older
3001
3002
3003
3004
3005
3006
3007
3008
3009
3010
3011
3012
3013
3014
3015
3016
3017
3018
3019
3020
3021
3022
3023
3024
3025
3026
3027
option = self.option
active = nodes.active
if option == 'FROM_ACTIVE':
if active:
src_label = active.label
for node in [n for n in nodes if n.select and nodes.active != n]:
node.label = src_label
elif option == 'FROM_NODE':
selected = [n for n in nodes if n.select]
for node in selected:
for input in node.inputs:
if input.links:
src = input.links[0].from_node
node.label = src.label
break
elif option == 'FROM_SOCKET':
selected = [n for n in nodes if n.select]
for node in selected:
for input in node.inputs:
if input.links:
src = input.links[0].from_socket
node.label = src.name
break
return {'FINISHED'}
class NWClearLabel(Operator, NWBase):
bl_idname = "node.nw_clear_label"
bl_label = "Clear Label"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
nodes, links = get_nodes_links(context)
for node in [n for n in nodes if n.select]:
node.label = ''
return {'FINISHED'}
def invoke(self, context, event):
if self.option:
return self.execute(context)
else:
return context.window_manager.invoke_confirm(self, event)
class NWModifyLabels(Operator, NWBase):
"""Modify Labels of all selected nodes"""
bl_idname = "node.nw_modify_labels"
bl_label = "Modify Labels"
bl_options = {'REGISTER', 'UNDO'}
prepend: StringProperty(
name="Add to Beginning"
)
append: StringProperty(
name="Add to End"
)
replace_from: StringProperty(
name="Text to Replace"
)
replace_to: StringProperty(
name="Replace with"
)
def execute(self, context):
nodes, links = get_nodes_links(context)
for node in [n for n in nodes if n.select]:
node.label = self.prepend + node.label.replace(self.replace_from, self.replace_to) + self.append
return {'FINISHED'}
def invoke(self, context, event):
self.prepend = ""
self.append = ""
self.remove = ""
return context.window_manager.invoke_props_dialog(self)
class NWAddTextureSetup(Operator, NWBase):
bl_idname = "node.nw_add_texture"
bl_label = "Texture Setup"
bl_description = "Add Texture Node Setup to Selected Shaders"
bl_options = {'REGISTER', 'UNDO'}
add_mapping: BoolProperty(name="Add Mapping Nodes", description="Create coordinate and mapping nodes for the texture (ignored for selected texture nodes)", default=True)
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
space = context.space_data
Brecht Van Lommel
committed
if space.tree_type == 'ShaderNodeTree':
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
shader_types = [x[1] for x in shaders_shader_nodes_props if x[1] not in {'MIX_SHADER', 'ADD_SHADER'}]
texture_types = [x[1] for x in shaders_texture_nodes_props]
selected_nodes = [n for n in nodes if n.select]
for t_node in selected_nodes:
valid = False
input_index = 0
if t_node.inputs:
for index, i in enumerate(t_node.inputs):
if not i.is_linked:
3111
3112
3113
3114
3115
3116
3117
3118
3119
3120
3121
3122
3123
3124
3125
3126
3127
3128
3129
3130
3131
3132
3133
3134
3135
3136
3137
3138
3139
3140
3141
3142
3143
3144
3145
3146
input_index = index
break
if valid:
locx = t_node.location.x
locy = t_node.location.y - t_node.dimensions.y/2
xoffset = [500, 700]
is_texture = False
if t_node.type in texture_types + ['MAPPING']:
xoffset = [290, 500]
is_texture = True
coordout = 2
image_type = 'ShaderNodeTexImage'
if (t_node.type in texture_types and t_node.type != 'TEX_IMAGE') or (t_node.type == 'BACKGROUND'):
coordout = 0 # image texture uses UVs, procedural textures and Background shader use Generated
if t_node.type == 'BACKGROUND':
image_type = 'ShaderNodeTexEnvironment'
if not is_texture:
tex = nodes.new(image_type)
tex.location = [locx - 200, locy + 112]
nodes.active = tex
links.new(tex.outputs[0], t_node.inputs[input_index])
t_node.select = False
if self.add_mapping or is_texture:
if t_node.type != 'MAPPING':
m = nodes.new('ShaderNodeMapping')
m.location = [locx - xoffset[0], locy + 141]
m.width = 240
else:
m = t_node
coord = nodes.new('ShaderNodeTexCoord')
coord.location = [locx - (200 if t_node.type == 'MAPPING' else xoffset[1]), locy + 124]
if not is_texture:
links.new(m.outputs[0], tex.inputs[0])
links.new(coord.outputs[coordout], m.inputs[0])
else:
nodes.active = m
links.new(m.outputs[0], t_node.inputs[input_index])
links.new(coord.outputs[coordout], m.inputs[0])
self.report({'WARNING'}, "No free inputs for node: "+t_node.name)
class NWAddPrincipledSetup(Operator, NWBase, ImportHelper):
bl_idname = "node.nw_add_textures_for_principled"
bl_label = "Principled Texture Setup"
bl_description = "Add Texture Node Setup for Principled BSDF"
bl_options = {'REGISTER', 'UNDO'}
directory: StringProperty(
name='Directory',
subtype='DIR_PATH',
default='',
description='Folder to search in for image files'
)
files: CollectionProperty(
type=bpy.types.OperatorFileListElement,
options={'HIDDEN', 'SKIP_SAVE'}
)
Santeri Salmijärvi
committed
relative_path: BoolProperty(
name='Relative Path',
description='Select the file relative to the blend file',
default=True
)
order = [
"filepath",
"files",
Santeri Salmijärvi
committed
def draw(self, context):
layout = self.layout
layout.alignment = 'LEFT'
layout.prop(self, 'relative_path')
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
space = context.space_data
Brecht Van Lommel
committed
if space.tree_type == 'ShaderNodeTree':
valid = True
return valid
def execute(self, context):
# Check if everything is ok
if not self.directory:
self.report({'INFO'}, 'No Folder Selected')
return {'CANCELLED'}
if not self.files[:]:
self.report({'INFO'}, 'No Files Selected')
return {'CANCELLED'}
nodes, links = get_nodes_links(context)
active_node = nodes.active
if not (active_node and active_node.bl_idname == 'ShaderNodeBsdfPrincipled'):
self.report({'INFO'}, 'Select Principled BSDF')
return {'CANCELLED'}
# Helper_functions
def split_into__components(fname):
# Split filename into components
# 'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
# Remove extension
fname = path.splitext(fname)[0]
# Remove digits
fname = ''.join(i for i in fname if not i.isdigit())
# Separate CamelCase by space
fname = re.sub("([a-z])([A-Z])","\g<1> \g<2>",fname)
# Replace common separators with SPACE
seperators = ['_', '.', '-', '__', '--', '#']
for sep in seperators:
fname = fname.replace(sep, ' ')
components = fname.split(' ')
components = [c.lower() for c in components]
return components
# Filter textures names for texturetypes in filenames
# [Socket Name, [abbreviations and keyword list], Filename placeholder]
tags = context.preferences.addons[__name__].preferences.principled_tags
normal_abbr = tags.normal.split(' ')
bump_abbr = tags.bump.split(' ')
gloss_abbr = tags.gloss.split(' ')
rough_abbr = tags.rough.split(' ')
['Displacement', tags.displacement.split(' '), None],
['Base Color', tags.base_color.split(' '), None],
['Subsurface Color', tags.sss_color.split(' '), None],
['Metallic', tags.metallic.split(' '), None],
['Specular', tags.specular.split(' '), None],
['Roughness', rough_abbr + gloss_abbr, None],
['Normal', normal_abbr + bump_abbr, None],
]
# Look through texture_types and set value as filename of first matched file
def match_files_to_socket_names():
for sname in socketnames:
for file in self.files:
fname = file.name
filenamecomponents = split_into__components(fname)
matches = set(sname[1]).intersection(set(filenamecomponents))
# TODO: ignore basename (if texture is named "fancy_metal_nor", it will be detected as metallic map, not normal map)
if matches:
sname[2] = fname
break
match_files_to_socket_names()
# Remove socketnames without found files
socketnames = [s for s in socketnames if s[2]
and path.exists(self.directory+s[2])]
if not socketnames:
self.report({'INFO'}, 'No matching images found')
print('No matching images found')
return {'CANCELLED'}
Santeri Salmijärvi
committed
# Don't override path earlier as os.path is used to check the absolute path
import_path = self.directory
if self.relative_path:
if bpy.data.filepath:
import_path = bpy.path.relpath(self.directory)
else:
self.report({'WARNING'}, 'Relative paths cannot be used with unsaved scenes!')
print('Relative paths cannot be used with unsaved scenes!')
# Add found images
print('\nMatched Textures:')
texture_nodes = []
disp_texture = None
normal_node = None
roughness_node = None
for i, sname in enumerate(socketnames):
print(i, sname[0], sname[2])
# DISPLACEMENT NODES
if sname[0] == 'Displacement':
disp_texture = nodes.new(type='ShaderNodeTexImage')
Santeri Salmijärvi
committed
img = bpy.data.images.load(path.join(import_path, sname[2]))
disp_texture.image = img
disp_texture.label = 'Displacement'
if disp_texture.image:
disp_texture.image.colorspace_settings.is_data = True
# Add displacement offset nodes
disp_node = nodes.new(type='ShaderNodeDisplacement')
disp_node.location = active_node.location + Vector((0, -560))
link = links.new(disp_node.inputs[0], disp_texture.outputs[0])
# TODO Turn on true displacement in the material
# Too complicated for now
output_node = [n for n in nodes if n.bl_idname == 'ShaderNodeOutputMaterial']
if output_node:
if not output_node[0].inputs[2].is_linked:
link = links.new(output_node[0].inputs[2], disp_node.outputs[0])
continue
if not active_node.inputs[sname[0]].is_linked:
# No texture node connected -> add texture node with new image
texture_node = nodes.new(type='ShaderNodeTexImage')
Santeri Salmijärvi
committed
img = bpy.data.images.load(path.join(import_path, sname[2]))
3322
3323
3324
3325
3326
3327
3328
3329
3330
3331
3332
3333
3334
3335
3336
3337
3338
3339
3340
3341
3342
3343
3344
3345
3346
3347
3348
3349
3350
3351
3352
3353
3354
3355
3356
3357
3358
3359
3360
3361
3362
3363
3364
texture_node.image = img
# NORMAL NODES
if sname[0] == 'Normal':
# Test if new texture node is normal or bump map
fname_components = split_into__components(sname[2])
match_normal = set(normal_abbr).intersection(set(fname_components))
match_bump = set(bump_abbr).intersection(set(fname_components))
if match_normal:
# If Normal add normal node in between
normal_node = nodes.new(type='ShaderNodeNormalMap')
link = links.new(normal_node.inputs[1], texture_node.outputs[0])
elif match_bump:
# If Bump add bump node in between
normal_node = nodes.new(type='ShaderNodeBump')
link = links.new(normal_node.inputs[2], texture_node.outputs[0])
link = links.new(active_node.inputs[sname[0]], normal_node.outputs[0])
normal_node_texture = texture_node
elif sname[0] == 'Roughness':
# Test if glossy or roughness map
fname_components = split_into__components(sname[2])
match_rough = set(rough_abbr).intersection(set(fname_components))
match_gloss = set(gloss_abbr).intersection(set(fname_components))
if match_rough:
# If Roughness nothing to to
link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
elif match_gloss:
# If Gloss Map add invert node
invert_node = nodes.new(type='ShaderNodeInvert')
link = links.new(invert_node.inputs[1], texture_node.outputs[0])
link = links.new(active_node.inputs[sname[0]], invert_node.outputs[0])
roughness_node = texture_node
else:
# This is a simple connection Texture --> Input slot
link = links.new(active_node.inputs[sname[0]], texture_node.outputs[0])
# Use non-color for all but 'Base Color' Textures
if not sname[0] in ['Base Color'] and texture_node.image:
texture_node.image.colorspace_settings.is_data = True
else:
# If already texture connected. add to node list for alignment
texture_node = active_node.inputs[sname[0]].links[0].from_node
# This are all connected texture nodes
texture_nodes.append(texture_node)
texture_node.label = sname[0]
if disp_texture:
texture_nodes.append(disp_texture)
# Alignment
for i, texture_node in enumerate(texture_nodes):
offset = Vector((-550, (i * -280) + 200))
texture_node.location = active_node.location + offset
if normal_node:
# Extra alignment if normal node was added
normal_node.location = normal_node_texture.location + Vector((300, 0))
if roughness_node:
# Alignment of invert node if glossy map
invert_node.location = roughness_node.location + Vector((300, 0))
# Add texture input + mapping
mapping = nodes.new(type='ShaderNodeMapping')
mapping.location = active_node.location + Vector((-1050, 0))
if len(texture_nodes) > 1:
# If more than one texture add reroute node in between
reroute = nodes.new(type='NodeReroute')
texture_nodes.append(reroute)
tex_coords = Vector((texture_nodes[0].location.x, sum(n.location.y for n in texture_nodes)/len(texture_nodes)))
reroute.location = tex_coords + Vector((-50, -120))
for texture_node in texture_nodes:
link = links.new(texture_node.inputs[0], reroute.outputs[0])
link = links.new(reroute.inputs[0], mapping.outputs[0])
else:
link = links.new(texture_nodes[0].inputs[0], mapping.outputs[0])
# Connect texture_coordiantes to mapping node
texture_input = nodes.new(type='ShaderNodeTexCoord')
texture_input.location = mapping.location + Vector((-200, 0))
link = links.new(mapping.inputs[0], texture_input.outputs[2])
# Create frame around tex coords and mapping
frame = nodes.new(type='NodeFrame')
frame.label = 'Mapping'
mapping.parent = frame
texture_input.parent = frame
frame.update()
# Create frame around texture nodes
frame = nodes.new(type='NodeFrame')
frame.label = 'Textures'
for tnode in texture_nodes:
tnode.parent = frame
frame.update()
# Just to be sure
active_node.select = False
nodes.update()
links.update()
force_update(context)
return {'FINISHED'}
class NWAddReroutes(Operator, NWBase):
"""Add Reroute Nodes and link them to outputs of selected nodes"""
bl_idname = "node.nw_add_reroutes"
bl_label = "Add Reroutes"
bl_description = "Add Reroutes to Outputs"
bl_options = {'REGISTER', 'UNDO'}
name="option",
items=[
('ALL', 'to all', 'Add to all outputs'),
('LOOSE', 'to loose', 'Add only to loose outputs'),
('LINKED', 'to linked', 'Add only to linked outputs'),
]
)
3449
3450
3451
3452
3453
3454
3455
3456
3457
3458
3459
3460
3461
3462
3463
3464
3465
3466
3467
3468
3469
3470
3471
3472
3473
3474
3475
3476
3477
3478
3479
3480
3481
3482
3483
3484
3485
3486
3487
3488
3489
def execute(self, context):
tree_type = context.space_data.node_tree.type
option = self.option
nodes, links = get_nodes_links(context)
# output valid when option is 'all' or when 'loose' output has no links
valid = False
post_select = [] # nodes to be selected after execution
# create reroutes and recreate links
for node in [n for n in nodes if n.select]:
if node.outputs:
x = node.location.x
y = node.location.y
width = node.width
# unhide 'REROUTE' nodes to avoid issues with location.y
if node.type == 'REROUTE':
node.hide = False
# When node is hidden - width_hidden not usable.
# Hack needed to calculate real width
if node.hide:
bpy.ops.node.select_all(action='DESELECT')
helper = nodes.new('NodeReroute')
helper.select = True
node.select = True
# resize node and helper to zero. Then check locations to calculate width
bpy.ops.transform.resize(value=(0.0, 0.0, 0.0))
width = 2.0 * (helper.location.x - node.location.x)
# restore node location
node.location = x, y
# delete helper
node.select = False
# only helper is selected now
bpy.ops.node.delete()
x = node.location.x + width + 20.0
if node.type != 'REROUTE':
y -= 35.0
y_offset = -22.0
loc = x, y
reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes
for out_i, output in enumerate(node.outputs):
pass_used = False # initial value to be analyzed if 'R_LAYERS'
# if node != 'R_LAYERS' - "pass_used" not needed, so set it to True
if node.type != 'R_LAYERS':
pass_used = True
else: # if 'R_LAYERS' check if output represent used render pass
node_scene = node.scene
node_layer = node.layer
# If output - "Alpha" is analyzed - assume it's used. Not represented in passes.
if output.name == 'Alpha':
pass_used = True
else:
# check entries in global 'rl_outputs' variable
for rlo in rl_outputs:
if output.name in {rlo.output_name, rlo.exr_output_name}:
pass_used = getattr(node_scene.view_layers[node_layer], rlo.render_pass)
3504
3505
3506
3507
3508
3509
3510
3511
3512
3513
3514
3515
3516
3517
3518
3519
3520
3521
3522
3523
3524
3525
3526
3527
3528
3529
3530
3531
3532
3533
break
if pass_used:
valid = ((option == 'ALL') or
(option == 'LOOSE' and not output.links) or
(option == 'LINKED' and output.links))
# Add reroutes only if valid, but offset location in all cases.
if valid:
n = nodes.new('NodeReroute')
nodes.active = n
for link in output.links:
links.new(n.outputs[0], link.to_socket)
links.new(output, n.inputs[0])
n.location = loc
post_select.append(n)
reroutes_count += 1
y += y_offset
loc = x, y
# disselect the node so that after execution of script only newly created nodes are selected
node.select = False
# nicer reroutes distribution along y when node.hide
if node.hide:
y_translate = reroutes_count * y_offset / 2.0 - y_offset - 35.0
for reroute in [r for r in nodes if r.select]:
reroute.location.y -= y_translate
for node in post_select:
node.select = True
return {'FINISHED'}
class NWLinkActiveToSelected(Operator, NWBase):
"""Link active node to selected nodes basing on various criteria"""
bl_idname = "node.nw_link_active_to_selected"
bl_label = "Link Active Node to Selected"
bl_options = {'REGISTER', 'UNDO'}
replace: BoolProperty()
use_node_name: BoolProperty()
use_outputs_names: BoolProperty()
@classmethod
def poll(cls, context):
valid = False
if nw_check(context):
if context.active_node is not None:
3549
3550
3551
3552
3553
3554
3555
3556
3557
3558
3559
3560
3561
3562
3563
3564
3565
3566
3567
3568
3569
if context.active_node.select:
valid = True
return valid
def execute(self, context):
nodes, links = get_nodes_links(context)
replace = self.replace
use_node_name = self.use_node_name
use_outputs_names = self.use_outputs_names
active = nodes.active
selected = [node for node in nodes if node.select and node != active]
outputs = [] # Only usable outputs of active nodes will be stored here.
for out in active.outputs:
if active.type != 'R_LAYERS':
outputs.append(out)
else:
# 'R_LAYERS' node type needs special handling.
# outputs of 'R_LAYERS' are callable even if not seen in UI.
# Only outputs that represent used passes should be taken into account
# Check if pass represented by output is used.
# global 'rl_outputs' list will be used for that
pass_used = False # initial value. Will be set to True if pass is used
if out.name == 'Alpha':
# Alpha output is always present. Doesn't have representation in render pass. Assume it's used.
pass_used = True
elif out.name in {rlo.output_name, rlo.exr_output_name}:
# example 'render_pass' entry: 'use_pass_uv' Check if True in scene render layers
pass_used = getattr(active.scene.view_layers[active.layer], rlo.render_pass)
break
if pass_used:
outputs.append(out)
doit = True # Will be changed to False when links successfully added to previous output.
for out in outputs:
if doit:
for node in selected:
dst_name = node.name # Will be compared with src_name if needed.
# When node has label - use it as dst_name
if node.label:
dst_name = node.label
valid = True # Initial value. Will be changed to False if names don't match.
src_name = dst_name # If names not used - this asignment will keep valid = True.
if use_node_name:
# Set src_name to source node name or label
src_name = active.name
if active.label:
src_name = active.label
elif use_outputs_names:
src_name = (out.name, )
for rlo in rl_outputs:
if out.name in {rlo.output_name, rlo.exr_output_name}:
src_name = (rlo.output_name, rlo.exr_output_name)
if dst_name not in src_name:
valid = False
if valid:
for input in node.inputs:
if input.type == out.type or node.type == 'REROUTE':
if replace or not input.is_linked:
links.new(out, input)
if not use_node_name and not use_outputs_names:
doit = False
break
return {'FINISHED'}
class NWAlignNodes(Operator, NWBase):
'''Align the selected nodes neatly in a row/column'''
bl_idname = "node.nw_align_nodes"
bl_options = {'REGISTER', 'UNDO'}
margin: IntProperty(name='Margin', default=50, description='The amount of space between nodes')
def execute(self, context):
nodes, links = get_nodes_links(context)
selection = []
for node in nodes:
if node.select and node.type != 'FRAME':
selection.append(node)
# If no nodes are selected, align all nodes
active_loc = None
if not selection:
selection = nodes
elif nodes.active in selection:
active_loc = copy(nodes.active.location) # make a copy, not a reference
# Check if nodes should be laid out horizontally or vertically
x_locs = [n.location.x + (n.dimensions.x / 2) for n in selection] # use dimension to get center of node, not corner
y_locs = [n.location.y - (n.dimensions.y / 2) for n in selection]
x_range = max(x_locs) - min(x_locs)
y_range = max(y_locs) - min(y_locs)
mid_x = (max(x_locs) + min(x_locs)) / 2
mid_y = (max(y_locs) + min(y_locs)) / 2
horizontal = x_range > y_range
# Sort selection by location of node mid-point
if horizontal:
selection = sorted(selection, key=lambda n: n.location.x + (n.dimensions.x / 2))
else:
selection = sorted(selection, key=lambda n: n.location.y - (n.dimensions.y / 2), reverse=True)
# Alignment
current_pos = 0
for node in selection:
current_margin = margin
current_margin = current_margin * 0.5 if node.hide else current_margin # use a smaller margin for hidden nodes
if horizontal:
node.location.x = current_pos
current_pos += current_margin + node.dimensions.x
node.location.y = mid_y + (node.dimensions.y / 2)
else:
node.location.y = current_pos
current_pos -= (current_margin * 0.3) + node.dimensions.y # use half-margin for vertical alignment
node.location.x = mid_x - (node.dimensions.x / 2)
# If active node is selected, center nodes around it
if active_loc is not None:
active_loc_diff = active_loc - nodes.active.location
for node in selection:
node.location += active_loc_diff
else: # Position nodes centered around where they used to be
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])
new_mid = (max(locs) + min(locs)) / 2
for node in selection:
if horizontal:
node.location.x += (mid_x - new_mid)
else:
node.location.y += (mid_y - new_mid)
return {'FINISHED'}
class NWSelectParentChildren(Operator, NWBase):
bl_idname = "node.nw_select_parent_child"
bl_label = "Select Parent or Children"
bl_options = {'REGISTER', 'UNDO'}
name="option",
items=(
('PARENT', 'Select Parent', 'Select Parent Frame'),
('CHILD', 'Select Children', 'Select members of selected frame'),
)
)
def execute(self, context):
nodes, links = get_nodes_links(context)
option = self.option
selected = [node for node in nodes if node.select]
if option == 'PARENT':
for sel in selected:
parent = sel.parent
if parent:
parent.select = True
else: # option == 'CHILD'
for sel in selected:
children = [node for node in nodes if node.parent == sel]
for kid in children:
kid.select = True
return {'FINISHED'}
class NWDetachOutputs(Operator, NWBase):
"""Detach outputs of selected node leaving inputs linked"""
bl_idname = "node.nw_detach_outputs"
def execute(self, context):
nodes, links = get_nodes_links(context)
selected = context.selected_nodes
bpy.ops.node.duplicate_move_keep_inputs()
new_nodes = context.selected_nodes
bpy.ops.node.select_all(action="DESELECT")
for node in selected:
node.select = True
bpy.ops.node.delete_reconnect()
for new_node in new_nodes:
new_node.select = True
Bartek Skorupa
committed
bpy.ops.transform.translate('INVOKE_DEFAULT')
Bartek Skorupa
committed
class NWLinkToOutputNode(Operator):
"""Link to Composite node or Material Output node"""
bl_idname = "node.nw_link_out"
bl_label = "Connect to Output"
if nw_check(context) and context.space_data.tree_type != 'GeometryNodeTree':
if context.active_node is not None:
for out in context.active_node.outputs:
if is_visible_socket(out):
valid = True
break
def execute(self, context):
nodes, links = get_nodes_links(context)
active = nodes.active
output_node = None
output_index = None
tree_type = context.space_data.tree_type
output_types_shaders = [x[1] for x in shaders_output_nodes_props]
output_types_compo = ['COMPOSITE']
output_types_blender_mat = ['OUTPUT']
output_types_textures = ['OUTPUT']
output_types = output_types_shaders + output_types_compo + output_types_blender_mat
if node.type in output_types:
output_node = node
break
if not output_node:
bpy.ops.node.select_all(action="DESELECT")
if tree_type == 'ShaderNodeTree':
Brecht Van Lommel
committed
output_node = nodes.new('ShaderNodeOutputMaterial')
elif tree_type == 'CompositorNodeTree':
output_node = nodes.new('CompositorNodeComposite')
elif tree_type == 'TextureNodeTree':
output_node = nodes.new('TextureNodeOutput')
output_node.location.x = active.location.x + active.dimensions.x + 80
output_node.location.y = active.location.y
if (output_node and active.outputs):
for i, output in enumerate(active.outputs):
if is_visible_socket(output):
output_index = i
break
for i, output in enumerate(active.outputs):
if output.type == output_node.inputs[0].type and is_visible_socket(output):
out_input_index = 0
Brecht Van Lommel
committed
if tree_type == 'ShaderNodeTree':
if active.outputs[output_index].name == 'Volume':
out_input_index = 1
elif active.outputs[output_index].type != 'SHADER': # connect to displacement if not a shader
out_input_index = 2
links.new(active.outputs[output_index], output_node.inputs[out_input_index])
force_update(context) # viewport render does not update
class NWMakeLink(Operator, NWBase):
"""Make a link from one socket to another"""
bl_idname = 'node.nw_make_link'
bl_label = 'Make Link'
bl_options = {'REGISTER', 'UNDO'}
from_socket: IntProperty()
to_socket: IntProperty()
def execute(self, context):
nodes, links = get_nodes_links(context)
n1 = nodes[context.scene.NWLazySource]
n2 = nodes[context.scene.NWLazyTarget]
links.new(n1.outputs[self.from_socket], n2.inputs[self.to_socket])
return {'FINISHED'}
class NWCallInputsMenu(Operator, NWBase):
"""Link from this output"""
bl_idname = 'node.nw_call_inputs_menu'
bl_label = 'Make Link'
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
nodes, links = get_nodes_links(context)
context.scene.NWSourceSocket = self.from_socket
n1 = nodes[context.scene.NWLazySource]
n2 = nodes[context.scene.NWLazyTarget]
if len(n2.inputs) > 1:
bpy.ops.wm.call_menu("INVOKE_DEFAULT", name=NWConnectionListInputs.bl_idname)
elif len(n2.inputs) == 1:
links.new(n1.outputs[self.from_socket], n2.inputs[0])
return {'FINISHED'}
class NWAddSequence(Operator, NWBase, ImportHelper):
"""Add an Image Sequence"""
bl_idname = 'node.nw_add_sequence'
bl_label = 'Import Image Sequence'
bl_options = {'REGISTER', 'UNDO'}
directory: StringProperty(
subtype="DIR_PATH"
)
filename: StringProperty(
subtype="FILE_NAME"
)
files: CollectionProperty(
type=bpy.types.OperatorFileListElement,
options={'HIDDEN', 'SKIP_SAVE'}
)
def execute(self, context):
nodes, links = get_nodes_links(context)
directory = self.directory
filename = self.filename
# DEBUG
# print ("\nDIR:", directory)
# print ("FN:", filename)
# print ("Fs:", list(f.name for f in files), '\n')
node_type = "CompositorNodeImage"
else:
self.report({'ERROR'}, "Unsupported Node Tree type!")
return {'CANCELLED'}
if not files[0].name and not filename:
self.report({'ERROR'}, "No file chosen")
return {'CANCELLED'}
elif files[0].name and (not filename or not path.exists(directory+filename)):
# User has selected multiple files without an active one, or the active one is non-existant
filename = files[0].name
if not path.exists(directory+filename):
self.report({'ERROR'}, filename+" does not exist!")
return {'CANCELLED'}
without_ext = '.'.join(filename.split('.')[:-1])
# if last digit isn't a number, it's not a sequence
self.report({'ERROR'}, filename+" does not seem to be part of a sequence")
return {'CANCELLED'}
extension = filename.split('.')[-1]
reverse = without_ext[::-1] # reverse string
count_numbers = 0
for char in reverse:
without_num = without_ext[:count_numbers*-1]
files = sorted(glob(directory + without_num + "[0-9]"*count_numbers + "." + extension))
nodes_list = [node for node in nodes]
if nodes_list:
nodes_list.sort(key=lambda k: k.location.x)
xloc = nodes_list[0].location.x - 220 # place new nodes at far left
yloc = 0
for node in nodes:
node.select = False
yloc += node_mid_pt(node, 'y')
yloc = yloc/len(nodes)
else:
xloc = 0
yloc = 0
name_with_hashes = without_num + "#"*count_numbers + '.' + extension
bpy.ops.node.add_node('INVOKE_DEFAULT', use_transform=True, type=node_type)
node.label = name_with_hashes
img = bpy.data.images.load(directory+(without_ext+'.'+extension))
img.source = 'SEQUENCE'
img.name = name_with_hashes
image_user = node.image_user if tree.type == 'SHADER' else node
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
image_user.frame_duration = num_frames
return {'FINISHED'}
class NWAddMultipleImages(Operator, NWBase, ImportHelper):
"""Add multiple images at once"""
bl_idname = 'node.nw_add_multiple_images'
bl_label = 'Open Selected Images'
bl_options = {'REGISTER', 'UNDO'}
directory: StringProperty(
subtype="DIR_PATH"
)
files: CollectionProperty(
type=bpy.types.OperatorFileListElement,
options={'HIDDEN', 'SKIP_SAVE'}
)
def execute(self, context):
nodes, links = get_nodes_links(context)
xloc, yloc = context.region.view2d.region_to_view(context.area.width/2, context.area.height/2)
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
if context.space_data.node_tree.type == 'SHADER':
node_type = "ShaderNodeTexImage"
elif context.space_data.node_tree.type == 'COMPOSITING':
node_type = "CompositorNodeImage"
else:
self.report({'ERROR'}, "Unsupported Node Tree type!")
return {'CANCELLED'}
new_nodes = []
for f in self.files:
fname = f.name
node = nodes.new(node_type)
new_nodes.append(node)
node.label = fname
node.hide = True
node.width_hidden = 100
node.location.x = xloc
node.location.y = yloc
yloc -= 40
img = bpy.data.images.load(self.directory+fname)
node.image = img
# shift new nodes up to center of tree
list_size = new_nodes[0].location.y - new_nodes[-1].location.y
for node in nodes:
if node in new_nodes:
node.select = True
node.location.y += (list_size/2)
else:
node.select = False
class NWViewerFocus(bpy.types.Operator):