render_toolbox/operators/output_setup.py

664 lines
27 KiB
Python
Executable File

import bpy
import os
import re
from pathlib import Path
from .. import fn
from bpy.props import (StringProperty,
BoolProperty,
EnumProperty,
CollectionProperty)
from ..constant import TECH_PASS_KEYWORDS
# region Search and replace
## -- Search and replace to batch rename item in collection property
class RT_OT_colprop_search_and_replace(bpy.types.Operator):
bl_idname = "rt.colprop_search_and_replace"
bl_label = "Search And Replace"
bl_description = "Search/Replace texts"
bl_options = {"REGISTER", "INTERNAL"}
## target to affect
data_path: StringProperty(name="Data Path", description="Path to collection prop to affect", default="")
target_prop: StringProperty(name="Target Prop", description="Name of the property to affect in whole collection", default="")
## Search and replace options
find: StringProperty(name="Find", description="Name to replace", default="", maxlen=0, options={'HIDDEN'}, subtype='NONE')
replace: StringProperty(name="Repl", description="New name placed", default="", maxlen=0, options={'HIDDEN'}, subtype='NONE')
prefix: BoolProperty(name="Prefix Only", description="Affect only prefix of name (skipping names without separator)", default=False)
use_regex: BoolProperty(name="Regex", description="Use regular expression (advanced), equivalent to python re.sub()", default=False)
separator: StringProperty(name="Separator", description="Separator to get prefix", default='_')
# selected: BoolProperty(name="Selected Only", description="Affect only selection", default=False)
def rename(self, source):
if not self.find:
return
old = source
if self.use_regex:
new = re.sub(self.find, self.replace, source)
if old != new:
return new
return
if self.prefix:
if not self.separator in source:
# Only if separator exists
return
splited = source.split(self.separator)
prefix = splited[0]
new_prefix = prefix.replace(self.find, self.replace)
if prefix != new_prefix:
splited[0] = new_prefix
return self.separator.join(splited)
else:
new = source.replace(self.find, self.replace)
if old != new:
return new
def execute(self, context):
## Get the collection prop from data path
collection_prop = eval(self.data_path)
count = 0
for item in collection_prop:
## Get the target prop
name = getattr(item, self.target_prop)
# prop = prop.replace(self.find, self.replace)
new = self.rename(name)
if new is None or name == new:
continue
## Rename in case of difference
print(f'rename: {name} --> {new}')
setattr(item, self.target_prop, new)
count += 1
if count:
mess = str(count) + ' rename(s)'
self.report({'INFO'}, mess)
else:
self.report({'WARNING'}, 'Nothing changed')
return{'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
row = layout.row()
# row_a= row.row()
# row_a.prop(self, "separator")
# # row_a.prop(self, "selected")
row_b= row.row()
row_b.prop(self, "prefix")
row_c= row.row()
row_c.prop(self, "use_regex")
# row_a.active = not self.use_regex
row_b.active = not self.use_regex
layout.prop(self, "find")
layout.prop(self, "replace")
# endregion
# region Create file output
## -- properties and operator for file output connect
class RT_PG_selectable_prop(bpy.types.PropertyGroup):
node_name: StringProperty(name="Node Name")
node_label: StringProperty(name="Node Label")
name: StringProperty(name="Name or Path")
socket_name: StringProperty(name="Source socket Name") # Source socket name as reference
select: BoolProperty(name="Selected", default=True)
is_linked: BoolProperty(name="Linked", default=False)
is_tech_pass: BoolProperty(name="Tech Pass", default=False)
# is_valid: BoolProperty(name="Valid", default=True)
## Specific to render layer nodes
scene_name : StringProperty(name="Scene Name", default='')
viewlayer_name : StringProperty(name="ViewLayer Name", default='')
## extra output naming options
name_from_node: StringProperty(name="Name From Node")
name_from_node_and_socket: StringProperty(name="Name From Node And Socket")
name_from_node_and_socket_with_scene: StringProperty(name="Name From Node And Socket With Scene prefixed")
name_from_node_and_socket_with_scene: StringProperty(name="Name From Node And Socket With Scene prefixed")
name_from_template: StringProperty(name="Name From Templates")
## Individual ? (not transmitted to assignation function currently, only used to build remap name)
# name_from_template: StringProperty(name="Layer Name From Templates")
# slot_from_template: StringProperty(name="File Slot From Templates")
def update_template_file_slot(self, context):
for item in context.window_manager.rt_socket_collection:
item.name_from_template = fn.build_path_from_template(
self.template_file_slot,
node_name=item.node_name,
node_label=item.node_label,
scene_name=item.scene_name,
viewlayer_name=item.viewlayer_name,
socket_name=item.socket_name,
)
def update_template_multilayer_name(self, context):
for item in context.window_manager.rt_socket_collection:
item.name_from_template = fn.build_path_from_template(
self.template_multilayer_name,
node_name=item.node_name,
node_label=item.node_label,
scene_name=item.scene_name,
viewlayer_name=item.viewlayer_name,
socket_name=item.socket_name,
)
class RT_OT_create_output_layers(bpy.types.Operator):
bl_idname = "rt.create_output_layers"
bl_label = "Create Output Layers"
bl_description = "Connect Selected Nodes to a new file output node\
\nIf an existing file output node is selected, socket are added to it"
bl_options = {"REGISTER", "UNDO"}
## ! collection prop -> Now stored on window manager at invoke
# socket_collection : CollectionProperty(type=RT_PG_selectable_prop)
show_custom_settings : BoolProperty(
name='Settings',
default=False)
split_tech_passes : BoolProperty(
name='Separate Tech Passes',
default=True,
description='Create a separate file output for technical passes (32 bit)')
## enum choice for naming: socket_name, node_name, node_and_socket_name,
name_type : EnumProperty(
name='Output Name From',
description='Choose the output name\
\nNode name use Label (Use node name when there is no Label)',
default='socket_name',
items=(
('node_and_socket_name', 'Node Socket Name', 'Use the node name prefix and socket name', 0),
('node_and_socket_name_with_scene', 'Scene + Node Socket Name', 'Use the node name prefix and socket name, prefix scene with render layer nodes', 1),
('socket_name', 'Socket Name', 'Use the socket name as output name', 2),
('node_name', 'Node Name', 'Use the node name as output name', 3),
# ('template', 'From Template', 'Use custom template for naming', 4), # Disable slot template for now
)
)
base_path : StringProperty(
name='Custom base path',
default='',
description='Set the base path of created file_output (not if already exists)')
file_format : EnumProperty(
name='Output Format',
default='NONE',
items=(
('NONE', 'Default', 'Use default settings'),
('OPEN_EXR_MULTILAYER', 'OpenEXR MultiLayer', 'Output image in multilayer OpenEXR format'),
('OPEN_EXR', 'OpenEXR', 'Output image in OpenEXR format'),
)
)
exr_codec : EnumProperty(
name='Codec',
default='DWAB',
description='Codec settings for OpenEXR',
items=(
('PIZ', 'PIZ (lossless)', ''),
('ZIP', 'ZIP (lossless)', ''),
('RLE', 'RLE (lossless)', ''),
('ZIPS', 'ZIPS (lossless)', ''),
('PXR24', 'Pxr24 (lossy)', ''),
('B44', 'B44 (lossy)', ''),
('B44A', 'B44A (lossy)', ''),
('DWAA', 'DWAA (lossy)', ''),
('DWAB', 'DWAB (lossy)', ''),
),
)
tech_exr_codec : EnumProperty(
name='Tech Passes Codec',
default='ZIP',
description='Codec settings for Techpasses OpenEXR (passes are set to 32bit)',
items=(
('PIZ', 'PIZ (lossless)', ''),
('ZIP', 'ZIP (lossless)', ''),
('RLE', 'RLE (lossless)', ''),
('ZIPS', 'ZIPS (lossless)', ''),
# ('PXR24', 'Pxr24 (lossy)', ''),
# ('B44', 'B44 (lossy)', ''),
# ('B44A', 'B44A (lossy)', ''),
# ('DWAA', 'DWAA (lossy)', ''),
# ('DWAB', 'DWAB (lossy)', ''),
),
)
color_depth : EnumProperty(
name='Color Depth',
default='16',
description='Bit depth per channel',
items=(
# ('8', '8', '8-bit color channels'),
# ('10', '10', '10-bit color channels'),
# ('12', '12', '12-bit color channels'),
('16', '16 (Half)', '16-bit color channels'),
('32', '32 (Full)', '32-bit color channels'),
),
)
# --- templates
use_base_path_templates : BoolProperty(
name='Use Base Path Template',
default=True,
description='Use template strings for base path formatting')
## -- base path
template_base_path : StringProperty(
name="Base Path Template",
description="Template for file output base path\
\nFolder containing subsequent filepath",
default="//render/{node_name}/",
# options={'SKIP_SAVE'}
)
## Multilayer base path : output on file instead of folder
template_multilayer_base_path : StringProperty(
name="Base Path Template",
description="Template for multilayer file output base paths\
\nOutput of the file sequence",
default="//render/{node_name}/{node_name}_",
# options={'SKIP_SAVE'}
)
## -- slots/layer_names (UNUSED for now)
template_file_slot : StringProperty(
name="File Slot Template",
description="Template for file output file slots\
\nSubpath added to base path for file sequence",
default="{socket_name}/{socket_name}_",
# update=update_template_file_slot,
)
## Multilayer's layer: layer name in EXR instead of subpath to file
template_multilayer_name : StringProperty(
name="Layer Name Template",
description="Template for multilayer file output layer names\
\nLayer name in EXR multilayer file",
default="{socket_name}",
# update=update_template_multilayer_name,
)
## extra suffix passes (unused)
# template_tech_pass_name : StringProperty(
# name="Tech Pass Name Template",
# description="Template for tech pass file output layer names\
# \nLayer name in EXR multilayer file",
# default="tech",
# )
# template_crypto_name : StringProperty(
# name="Crypto Pass Name Template",
# description="Template for crypto pass file output layer names\
# \nLayer name in EXR multilayer file",
# default="crypto",
# )
def invoke(self, context, event):
if not hasattr(context.window_manager, 'rt_socket_collection'):
## Create collection prop on window manager if not existing
## (not stored in self anymore, so that other operators can access it, ex: search and replace)
bpy.types.WindowManager.rt_socket_collection = CollectionProperty(type=RT_PG_selectable_prop)
self.socket_collection = context.window_manager.rt_socket_collection
self.socket_collection.clear()
if event.ctrl:
# Direct connect, do not use any options
self.base_path = ''
return self.execute(context)
## Store variable usable in templates
# self.blend_name = bpy.data.filepath
# self.version = ''
# if self.blend_name:
# self.blend_name = Path(self.blend_name).stem
# self.version = fn.get_rightmost_number_in_string(self.blend_name)
## tech_passes
prefs = fn.get_addon_prefs()
tech_passes_names = prefs.tech_passes_names
if prefs.use_env_technical_passes:
tech_passes_names = os.getenv('RENDERTOOLBOX_TECH_PASSES', prefs.tech_passes_names)
tech_passes_names = [tp.strip().lower() for tp in tech_passes_names.split(',') if tp.strip()]
selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE']
if not selected:
self.report({'ERROR'}, 'No render layer nodes selected')
return {'CANCELLED'}
for n in selected:
for o in n.outputs:
if o.is_unavailable:
continue
item = self.socket_collection.add()
item.node_name = n.name
item.node_label = n.label.strip()
item.socket_name = o.name
## Set editable names
item.name = o.name
## Store other naming options (cleaned at exec with bpy.path.clean_name)
node_name = n.label.strip() if n.label.strip() else n.name
scene_node_name = node_name # same by default
if n.type == 'R_LAYERS':
scene_node_name = f'{n.scene.name}_{n.layer}'
item.scene_name = n.scene.name
item.viewlayer_name = n.layer
## Change node_name for render layers: scene_viewlayer_name
if n.type == 'R_LAYERS' and node_name != n.label: # skip if a label is set
node_name = f'{n.layer}'
item.name_from_node = node_name
if len(n.outputs) == 1:
## Only one output, just pick node name, no need to add socket name
item.name_from_node_and_socket = node_name
item.name_from_node_and_socket_with_scene = scene_node_name
else:
# print(f'node_name: {node_name} VS {scene_node_name}')
item.name_from_node_and_socket = f'{node_name}_{o.name}'
item.name_from_node_and_socket_with_scene = f'{scene_node_name}_{o.name}'
## TODO: rename item.name according to template pairs in preferences (to add later)
if o.is_linked:
item.is_linked = True
item.select = False
if item.socket_name.lower() in tech_passes_names:
item.is_tech_pass = True
## Assign default template values (Disabled for now)
## TRIGGER update to firstly fill template slot/names
# self.template_file_slot = context.scene.render_toolbox.default_file_slot
# self.template_multilayer_name = context.scene.render_toolbox.default_multilayer_name
## Replace self base path
# self.template_base_path = context.scene.render_toolbox.default_base_path
# self.template_multilayer_base_path = context.scene.render_toolbox.default_multilayer_base_path
## For template preview display, Store some infos
ref = selected[0]
self.node_name = ref.name.strip()
self.node_label = ref.label.strip()
self.scene_name = ''
self.view_layer_name = ''
if ref.type == 'R_LAYERS':
self.scene_name = ref.scene.name
self.view_layer_name = ref.layer
self.final_base_path_template = ''
return context.window_manager.invoke_props_dialog(self, width=530)
def draw(self, context):
layout = self.layout
box = layout.box()
expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT'
box.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon)
## Settings
if self.show_custom_settings:
box.use_property_split = True
# Template settings
col = box.column()
row = col.row(align=True)
row.prop(self, 'use_base_path_templates')
if self.use_base_path_templates:
row.operator("rt.reset_path_templates", text='', icon='FILE_REFRESH')
row.separator()
row.operator("rt.info_note", text='', icon='INFO').text = """Format the base path using templates.
Possible variables:
{node_name} : name of the node
{node_label} : label of the node, fallback to node_name when nothing
{scene_name} : name of the scene from render layer node
{viewlayer_name} : name of the viewlayer from render layer node
{blend_name} : stem of the blend
{blend_version} : version (rightmost number in blend name)
{date} : (YYYYMMDD format)
{time} : (HHMMSS format)""" # {socket_name} : name of the socket (only availabe in slots)
final_format = self.file_format
if final_format == 'NONE':
final_format = os.environ.get('FILE_FORMAT', 'OPEN_EXR_MULTILAYER')
if self.use_base_path_templates:
if final_format == 'OPEN_EXR_MULTILAYER':
## self local template
# box.prop(self, 'template_multilayer_base_path')
# self.final_base_path_template = self.template_multilayer_base_path
## scene prop
box.prop(context.scene.render_toolbox, 'default_multilayer_base_path')
self.final_base_path_template = context.scene.render_toolbox.default_multilayer_base_path
else:
## self local template
# box.prop(self, 'template_base_path')
# self.final_base_path_template = self.template_base_path
## scene prop
box.prop(context.scene.render_toolbox, 'default_base_path')
self.final_base_path_template = context.scene.render_toolbox.default_base_path
## display the applied template
box.label(text=fn.build_path_from_template(self.final_base_path_template,
node_name = self.node_name,
node_label=self.node_label,
scene_name=self.scene_name,
viewlayer_name=self.view_layer_name,
),
icon='INFO'
)
else:
box.prop(self, 'base_path')
self.final_base_path_template = None
if self.name_type == 'template':
row_single = box.row()
row_single.prop(self, 'template_file_slot')
row_single.active = final_format != 'OPEN_EXR_MULTILAYER'
row_multi = box.row()
row_multi.prop(self, 'template_multilayer_name')
row_multi.active = final_format == 'OPEN_EXR_MULTILAYER'
box.row().prop(self, 'name_type', expand=False)
col = box.column()
col.prop(self, 'file_format')
col.prop(self, 'exr_codec')
col.row().prop(self, 'color_depth', expand=True)
col.prop(self, 'split_tech_passes')
row = col.row()
row.prop(self, 'tech_exr_codec', text='Tech Passes Codec')
row.active = self.split_tech_passes
# if self.split_tech_passes:
# col.prop(self, 'tech_exr_codec', text='Tech Passes Codec')
search_row = layout.row()
op = search_row.operator("rt.colprop_search_and_replace", icon='BORDERMOVE')
op.data_path = 'bpy.context.window_manager.rt_socket_collection'
if self.name_type == 'socket_name':
op.target_prop = 'name'
elif self.name_type == 'node_name':
op.target_prop = 'name_from_node'
elif self.name_type == 'node_and_socket_name':
op.target_prop = 'name_from_node_and_socket'
elif self.name_type == 'node_and_socket_name_with_scene':
op.target_prop = 'name_from_node_and_socket_with_scene'
elif self.name_type == 'template':
op.target_prop = 'name_from_template'
## Node Sockets
layout.use_property_split = False
col = layout.column()
current_node_name = ''
for item in self.socket_collection:
if item.node_name != current_node_name:
current_node_name = item.node_name
col.separator()
## Display node label + name or name only
if item.node_label:
display_node_name = f'{item.node_label} ({item.node_name})'
else:
display_node_name = item.node_name
## A bit dirty: For render layer node, show render layer-node name.
if display_node_name.startswith('Render Layer'):
rln = context.scene.node_tree.nodes.get(item.node_name)
display_node_name = f'{rln.scene.name}/{rln.layer} ({display_node_name})'
col.label(text=display_node_name, icon='NODE_SEL')
row = col.row()
row.active = item.select
if item.is_linked:
row.label(text='', icon='LINKED') # NODETREE
else:
row.label(text='', icon='BLANK1')
row.prop(item, 'select', text='')
display_name = item.socket_name
if 'crypto' in display_name.lower():
display_name = f'{display_name} -> 32bit output node'
elif self.split_tech_passes and item.is_tech_pass:
display_name = f'{display_name} -> Tech pass'
row.label(text=display_name)
row.label(text='', icon='RIGHTARROW')
if self.name_type == 'socket_name':
row.prop(item, 'name', text='')
elif self.name_type == 'node_name':
row.prop(item, 'name_from_node', text='')
elif self.name_type == 'node_and_socket_name':
row.prop(item, 'name_from_node_and_socket', text='')
elif self.name_type == 'node_and_socket_name_with_scene':
row.prop(item, 'name_from_node_and_socket_with_scene', text='')
elif self.name_type == 'template':
row.prop(item, 'name_from_template', text='')
if self.split_tech_passes:
row.prop(item, 'is_tech_pass', text='')
def execute(self, context):
# Build exclude dict from selection
excludes = {}
remap_names = {}
## Old system
# if len(self.socket_collection):
# for item in self.socket_collection:
# if not item.select:
# # All deselected goes to exclude with {node_name: [socket1, ...]}
# excludes.setdefault(item.node_name, []).append(item.name)
# elif item.socket_name != item.name:
# remap_names[item.socket_name] = item.name
tech_pass_names = []
if len(self.socket_collection):
for item in self.socket_collection:
final_name = item.name
## change name if other options were used
if self.name_type == 'node_name':
final_name = item.name_from_node
elif self.name_type == 'node_and_socket_name':
final_name = item.name_from_node_and_socket
elif self.name_type == 'node_and_socket_name_with_scene':
final_name = item.name_from_node_and_socket_with_scene
elif self.name_type == 'name_from_template':
final_name = item.name_from_template
if not item.select:
# All deselected goes to exclude with {node_name: [socket1, ...]}
excludes.setdefault(item.node_name, []).append(item.socket_name)
elif item.socket_name != final_name:
remap_names.setdefault(item.node_name, {})[item.socket_name] = final_name
# remap_names[item.socket_name] = final_name
if self.split_tech_passes and item.is_tech_pass:
tech_pass_names.append(item.socket_name.lower())
## Handle default file format
file_ext = self.file_format
if self.file_format == 'NONE':
env_file_format = os.environ.get('FILE_FORMAT')
file_ext = env_file_format if env_file_format else 'OPEN_EXR_MULTILAYER'
file_format = {
'file_format' : file_ext,
'exr_codec' : self.exr_codec,
'color_depth' : self.color_depth,
}
tech_file_format = {
## Force the use of OpenEXR for tech passes if normal passes uses PNG or the likes
'file_format' : file_ext if file_ext in ('OPEN_EXR_MULTILAYER', 'OPEN_EXR') else 'OPEN_EXR',
'exr_codec' : self.tech_exr_codec,
'color_depth' : '32',
}
scn = context.scene
nodes = scn.node_tree.nodes
selected = [n for n in nodes if n.select]
outfile = next((n for n in selected if n.type == 'OUTPUT_FILE'), None)
# Exclude output file from
selected = [n for n in selected if n.type != 'OUTPUT_FILE']
# fn.connect_to_file_output(selected, outfile)
for n in selected:
fn.connect_to_file_output(
n,
outfile,
base_path=self.base_path,
excludes=excludes,
remap_names=remap_names,
file_format=file_format,
tech_pass_names=tech_pass_names,
tech_file_format=tech_file_format,
template=self.final_base_path_template)
return {"FINISHED"}
# endregion
classes=(
RT_OT_colprop_search_and_replace,
RT_PG_selectable_prop,
RT_OT_create_output_layers,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)