add template in create output path base node
templates for slot are in code but disabled, not useful in the end
This commit is contained in:
parent
6ad75a664f
commit
37a9e0ca43
@ -2,7 +2,7 @@ bl_info = {
|
|||||||
"name": "Render Toolbox",
|
"name": "Render Toolbox",
|
||||||
"description": "Perform checks and setup outputs",
|
"description": "Perform checks and setup outputs",
|
||||||
"author": "Samuel Bernou",
|
"author": "Samuel Bernou",
|
||||||
"version": (0, 4, 0),
|
"version": (0, 5, 0),
|
||||||
"blender": (4, 0, 0),
|
"blender": (4, 0, 0),
|
||||||
"location": "View3D",
|
"location": "View3D",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
@ -11,10 +11,12 @@ bl_info = {
|
|||||||
"category": "Render"
|
"category": "Render"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
from . import properties
|
||||||
from . import operators
|
from . import operators
|
||||||
from . import ui
|
from . import ui
|
||||||
|
|
||||||
modules = (
|
modules = (
|
||||||
|
properties,
|
||||||
operators,
|
operators,
|
||||||
ui,
|
ui,
|
||||||
# prefs,
|
# prefs,
|
||||||
|
136
fn.py
136
fn.py
@ -4,6 +4,7 @@ import re
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from .constant import TECH_PASS_KEYWORDS
|
from .constant import TECH_PASS_KEYWORDS
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
# region Manage nodes
|
# region Manage nodes
|
||||||
|
|
||||||
@ -62,9 +63,11 @@ def recursive_node_connect_check(l, target_node):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# region create and connect file output
|
||||||
|
|
||||||
def create_and_connect_file_output(node, outputs, file_out, out_name, base_path, out_base,
|
def create_and_connect_file_output(node, outputs, file_out, out_name, base_path, out_base,
|
||||||
scene, remap_names, file_format, suffix='', color_depth=None,
|
scene, remap_names, file_format, suffix='', color_depth=None,
|
||||||
location_offset=(500, 50)):
|
location_offset=(500, 50), base_path_template=None, use_slot_template=False):
|
||||||
"""Helper function to create file output node and connect it to outputs
|
"""Helper function to create file output node and connect it to outputs
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -112,6 +115,24 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
|
|||||||
if node.parent:
|
if node.parent:
|
||||||
fo.parent = node.parent
|
fo.parent = node.parent
|
||||||
|
|
||||||
|
if base_path_template is not None:
|
||||||
|
fo.base_path = build_path_from_template(base_path_template,
|
||||||
|
node_name=node.name.strip(),
|
||||||
|
node_label=node.label.strip(),
|
||||||
|
scene_name=node.scene.name if node.type == 'R_LAYERS' else None,
|
||||||
|
viewlayer_name=node.layer if node.type == 'R_LAYERS' else None,
|
||||||
|
)
|
||||||
|
if suffix:
|
||||||
|
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||||
|
## insert suffix right after last slash as folder
|
||||||
|
path_list = re.split('(\\/)', fo.base_path)
|
||||||
|
path_list.insert(-1, suffix)
|
||||||
|
fo.base_path = ''.join(path_list)
|
||||||
|
else:
|
||||||
|
## Ensure slash separator then add suffix
|
||||||
|
fo.base_path = fo.base_path.rstrip('\\/') + '/' + suffix
|
||||||
|
else:
|
||||||
|
# default behavior without template
|
||||||
if base_path:
|
if base_path:
|
||||||
fo.base_path = base_path
|
fo.base_path = base_path
|
||||||
else:
|
else:
|
||||||
@ -134,9 +155,11 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
|
|||||||
|
|
||||||
ls = fo.layer_slots[-1]
|
ls = fo.layer_slots[-1]
|
||||||
ls.name = slot_name
|
ls.name = slot_name
|
||||||
|
|
||||||
fs = fo.file_slots[-1]
|
fs = fo.file_slots[-1]
|
||||||
fs.path = f'{slot_name}/{slot_name}_'
|
fs.path = f'{slot_name}/{slot_name}_'
|
||||||
|
if use_slot_template:
|
||||||
|
## Always False (currently never use slot template)
|
||||||
|
fs.path = slot_name
|
||||||
|
|
||||||
out_input = fo.inputs[-1]
|
out_input = fo.inputs[-1]
|
||||||
links.new(o, out_input)
|
links.new(o, out_input)
|
||||||
@ -147,7 +170,7 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
|
|||||||
|
|
||||||
|
|
||||||
def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None,
|
def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None,
|
||||||
file_format=None, split_tech_passes=False):
|
file_format=None, split_tech_passes=False, template=None):
|
||||||
"""Connect selected nodes output to file output(s)
|
"""Connect selected nodes output to file output(s)
|
||||||
if a file output is selected, add intputs on it
|
if a file output is selected, add intputs on it
|
||||||
|
|
||||||
@ -234,7 +257,8 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None
|
|||||||
fo_regular = create_and_connect_file_output(
|
fo_regular = create_and_connect_file_output(
|
||||||
node, regular_outputs, file_out, out_name, base_path,
|
node, regular_outputs, file_out, out_name, base_path,
|
||||||
out_base, scene, remap_names, file_format,
|
out_base, scene, remap_names, file_format,
|
||||||
location_offset=(500, y_offset)
|
location_offset=(500, y_offset),
|
||||||
|
base_path_template=template
|
||||||
)
|
)
|
||||||
if fo_regular and fo_regular not in created_nodes:
|
if fo_regular and fo_regular not in created_nodes:
|
||||||
created_nodes.append(fo_regular)
|
created_nodes.append(fo_regular)
|
||||||
@ -249,7 +273,8 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None
|
|||||||
node, tech_outputs, None, out_name, base_path,
|
node, tech_outputs, None, out_name, base_path,
|
||||||
out_base, scene, remap_names, tech_file_format,
|
out_base, scene, remap_names, tech_file_format,
|
||||||
suffix='tech/', color_depth='32',
|
suffix='tech/', color_depth='32',
|
||||||
location_offset=(500, y_offset)
|
location_offset=(500, y_offset),
|
||||||
|
base_path_template=template
|
||||||
)
|
)
|
||||||
|
|
||||||
if fo_tech and fo_tech not in created_nodes:
|
if fo_tech and fo_tech not in created_nodes:
|
||||||
@ -266,17 +291,19 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None
|
|||||||
node, crypto_outputs, None, out_name, base_path,
|
node, crypto_outputs, None, out_name, base_path,
|
||||||
out_base, scene, remap_names, tech_file_format,
|
out_base, scene, remap_names, tech_file_format,
|
||||||
suffix='cryptos/', color_depth='32',
|
suffix='cryptos/', color_depth='32',
|
||||||
location_offset=(500, y_offset)
|
location_offset=(500, y_offset),
|
||||||
|
base_path_template=template
|
||||||
)
|
)
|
||||||
|
|
||||||
if fo_crypto and fo_crypto not in created_nodes:
|
if fo_crypto and fo_crypto not in created_nodes:
|
||||||
created_nodes.append(fo_crypto)
|
created_nodes.append(fo_crypto)
|
||||||
|
|
||||||
|
|
||||||
return created_nodes
|
return created_nodes
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
|
||||||
# region Utilities
|
# region Utilities
|
||||||
|
|
||||||
@ -365,3 +392,98 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
|
|||||||
if isinstance(_message, str):
|
if isinstance(_message, str):
|
||||||
_message = [_message]
|
_message = [_message]
|
||||||
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
|
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
|
||||||
|
|
||||||
|
|
||||||
|
def get_rightmost_number_in_string(string) -> str:
|
||||||
|
"""Get the rightmost number passed string
|
||||||
|
return the number as string, empty string if no number found
|
||||||
|
"""
|
||||||
|
res = re.search(r'(\d+)(?!.*\d)', string)
|
||||||
|
if not res:
|
||||||
|
return ''
|
||||||
|
return res.group(1)
|
||||||
|
|
||||||
|
|
||||||
|
def build_path_from_template(template: str,
|
||||||
|
node_name: str=None,
|
||||||
|
node_label: str=None,
|
||||||
|
scene_name: str=None,
|
||||||
|
viewlayer_name: str=None,
|
||||||
|
socket_name: str=None,
|
||||||
|
socket_index: str=None,
|
||||||
|
) -> str:
|
||||||
|
"""Get a name from a template string
|
||||||
|
Possible keyword:
|
||||||
|
{node_name} : name of the node
|
||||||
|
{node_label} : label of the node (fallback to node_name if not set)
|
||||||
|
{scene_name} : name of the source render layer node scene
|
||||||
|
{viewlayer_name} : name of the source render layer viewlayer
|
||||||
|
{socket_name} : name of source the node socket
|
||||||
|
{socket_index} : index of the node in the node tree (010, 020, etc.)
|
||||||
|
|
||||||
|
## Info from Blend
|
||||||
|
{blend_name} : name of the blend file
|
||||||
|
{blend_version} : version of the blend file
|
||||||
|
{date} : current date (YYYYMMDD format)
|
||||||
|
{time} : current time (HHMMSS format)
|
||||||
|
"""
|
||||||
|
|
||||||
|
## note: socket index is a bad idea because the final order is not decided before execute
|
||||||
|
if not template:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
# Get blend file info
|
||||||
|
blend_path = bpy.data.filepath
|
||||||
|
blend_name = ''
|
||||||
|
blend_version = ''
|
||||||
|
|
||||||
|
if blend_path:
|
||||||
|
blend_name = Path(blend_path).stem
|
||||||
|
blend_version = get_rightmost_number_in_string(blend_name)
|
||||||
|
|
||||||
|
# Get current date and time
|
||||||
|
import datetime
|
||||||
|
now = datetime.datetime.now()
|
||||||
|
current_date = now.strftime("%Y%m%d")
|
||||||
|
current_time = now.strftime("%H%M%S")
|
||||||
|
|
||||||
|
# Handle node_label to name fallback
|
||||||
|
if "{node_label}" in template:
|
||||||
|
if node_label is None or not node_label:
|
||||||
|
node_label = node_name
|
||||||
|
|
||||||
|
if socket_index:
|
||||||
|
# Ensure socket_index is formatted as a zero-padded string and increment on base 10 (allow to insert inbetween renders)
|
||||||
|
socket_index = f"{int(socket_index)*10:03d}"
|
||||||
|
|
||||||
|
def clean_name(name):
|
||||||
|
# Clean names to be file-system safe
|
||||||
|
if name is None:
|
||||||
|
return ''
|
||||||
|
return bpy.path.clean_name(name)
|
||||||
|
|
||||||
|
# Create a dictionary of all available variables
|
||||||
|
template_vars = {
|
||||||
|
'node_name': clean_name(node_name),
|
||||||
|
'node_label': clean_name(node_label),
|
||||||
|
'socket_name': clean_name(socket_name),
|
||||||
|
'scene_name': clean_name(scene_name),
|
||||||
|
'viewlayer_name': clean_name(viewlayer_name),
|
||||||
|
'socket_index': socket_index or '',
|
||||||
|
'blend_name': clean_name(blend_name),
|
||||||
|
'blend_version': blend_version,
|
||||||
|
'date': current_date,
|
||||||
|
'time': current_time,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Apply template using format_map to handle missing keys gracefully
|
||||||
|
try:
|
||||||
|
applied_template = template.format_map(template_vars)
|
||||||
|
except KeyError as e:
|
||||||
|
# If a key is missing, return template with error message
|
||||||
|
# print(f"Template error: Unknown variable {e}")
|
||||||
|
return f"Template error: Unknown {e}"
|
||||||
|
|
||||||
|
return applied_template
|
||||||
|
|
||||||
|
# endregion
|
@ -1,6 +1,9 @@
|
|||||||
import bpy
|
import bpy
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from .. import fn
|
from .. import fn
|
||||||
from bpy.props import (StringProperty,
|
from bpy.props import (StringProperty,
|
||||||
BoolProperty,
|
BoolProperty,
|
||||||
@ -121,10 +124,42 @@ class RT_PG_selectable_prop(bpy.types.PropertyGroup):
|
|||||||
is_linked: BoolProperty(name="Linked", default=False)
|
is_linked: BoolProperty(name="Linked", default=False)
|
||||||
# is_valid: BoolProperty(name="Valid", default=True)
|
# 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
|
## extra output naming options
|
||||||
name_from_node: StringProperty(name="Name From Node")
|
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: 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_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):
|
class RT_OT_create_output_layers(bpy.types.Operator):
|
||||||
bl_idname = "rt.create_output_layers"
|
bl_idname = "rt.create_output_layers"
|
||||||
@ -150,20 +185,16 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
name='Output Name From',
|
name='Output Name From',
|
||||||
description='Choose the output name\
|
description='Choose the output name\
|
||||||
\nNode name use Label (Use node name when there is no Label)',
|
\nNode name use Label (Use node name when there is no Label)',
|
||||||
default='node_and_socket_name',
|
default='socket_name',
|
||||||
items=(
|
items=(
|
||||||
('node_and_socket_name', 'Node Socket Name', 'Use the node name prefix and socket name', 0),
|
('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),
|
('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),
|
('socket_name', 'Socket Name', 'Use the socket name as output name', 2),
|
||||||
('node_name', 'Node Name', 'Use the node name as output name', 3),
|
('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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# prefix_with_node_name : BoolProperty(
|
|
||||||
# name='Prefix With Node Name',
|
|
||||||
# description='Add the node name as prefix to the output name',
|
|
||||||
# default=False)
|
|
||||||
|
|
||||||
base_path : StringProperty(
|
base_path : StringProperty(
|
||||||
name='Custom base path',
|
name='Custom base path',
|
||||||
default='',
|
default='',
|
||||||
@ -209,6 +240,68 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- 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,
|
||||||
|
# options={'SKIP_SAVE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
## 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,
|
||||||
|
# options={'SKIP_SAVE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
## 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):
|
def invoke(self, context, event):
|
||||||
if not hasattr(context.window_manager, 'rt_socket_collection'):
|
if not hasattr(context.window_manager, 'rt_socket_collection'):
|
||||||
## Create collection prop on window manager if not existing
|
## Create collection prop on window manager if not existing
|
||||||
@ -222,6 +315,13 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
self.base_path = ''
|
self.base_path = ''
|
||||||
return self.execute(context)
|
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)
|
||||||
|
|
||||||
selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE']
|
selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE']
|
||||||
if not selected:
|
if not selected:
|
||||||
self.report({'ERROR'}, 'No render layer nodes selected')
|
self.report({'ERROR'}, 'No render layer nodes selected')
|
||||||
@ -244,6 +344,8 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
|
|
||||||
if n.type == 'R_LAYERS':
|
if n.type == 'R_LAYERS':
|
||||||
scene_node_name = f'{n.scene.name}_{n.layer}'
|
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
|
## 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
|
if n.type == 'R_LAYERS' and node_name != n.label: # skip if a label is set
|
||||||
@ -256,7 +358,7 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
item.name_from_node_and_socket = node_name
|
item.name_from_node_and_socket = node_name
|
||||||
item.name_from_node_and_socket_with_scene = scene_node_name
|
item.name_from_node_and_socket_with_scene = scene_node_name
|
||||||
else:
|
else:
|
||||||
print(f'node_name: {node_name} VS {scene_node_name}')
|
# 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 = f'{node_name}_{o.name}'
|
||||||
item.name_from_node_and_socket_with_scene = f'{scene_node_name}_{o.name}'
|
item.name_from_node_and_socket_with_scene = f'{scene_node_name}_{o.name}'
|
||||||
|
|
||||||
@ -265,6 +367,25 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
item.is_linked = True
|
item.is_linked = True
|
||||||
item.select = False
|
item.select = False
|
||||||
|
|
||||||
|
## 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=500)
|
return context.window_manager.invoke_props_dialog(self, width=500)
|
||||||
|
|
||||||
def draw(self, context):
|
def draw(self, context):
|
||||||
@ -276,8 +397,68 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
## Settings
|
## Settings
|
||||||
if self.show_custom_settings:
|
if self.show_custom_settings:
|
||||||
box.use_property_split = True
|
box.use_property_split = True
|
||||||
box.row().prop(self, 'name_type', expand=False)
|
|
||||||
|
# Template settings
|
||||||
|
col = box.column()
|
||||||
|
row = col.row(align=True)
|
||||||
|
row.prop(self, 'use_base_path_templates')
|
||||||
|
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')
|
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'
|
||||||
|
|
||||||
|
## Todo: Add rt.info_note to explain templates
|
||||||
|
box.row().prop(self, 'name_type', expand=False)
|
||||||
col = box.column()
|
col = box.column()
|
||||||
col.prop(self, 'file_format')
|
col.prop(self, 'file_format')
|
||||||
col.prop(self, 'exr_codec')
|
col.prop(self, 'exr_codec')
|
||||||
@ -295,6 +476,8 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
op.target_prop = 'name_from_node_and_socket'
|
op.target_prop = 'name_from_node_and_socket'
|
||||||
elif self.name_type == 'node_and_socket_name_with_scene':
|
elif self.name_type == 'node_and_socket_name_with_scene':
|
||||||
op.target_prop = 'name_from_node_and_socket_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
|
## Node Sockets
|
||||||
layout.use_property_split = False
|
layout.use_property_split = False
|
||||||
@ -339,6 +522,8 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
row.prop(item, 'name_from_node_and_socket', text='')
|
row.prop(item, 'name_from_node_and_socket', text='')
|
||||||
elif self.name_type == 'node_and_socket_name_with_scene':
|
elif self.name_type == 'node_and_socket_name_with_scene':
|
||||||
row.prop(item, 'name_from_node_and_socket_with_scene', text='')
|
row.prop(item, 'name_from_node_and_socket_with_scene', text='')
|
||||||
|
elif self.name_type == 'template':
|
||||||
|
row.prop(item, 'name_from_template', text='')
|
||||||
|
|
||||||
def execute(self, context):
|
def execute(self, context):
|
||||||
|
|
||||||
@ -364,6 +549,8 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
final_name = item.name_from_node_and_socket
|
final_name = item.name_from_node_and_socket
|
||||||
elif self.name_type == 'node_and_socket_name_with_scene':
|
elif self.name_type == 'node_and_socket_name_with_scene':
|
||||||
final_name = item.name_from_node_and_socket_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:
|
if not item.select:
|
||||||
# All deselected goes to exclude with {node_name: [socket1, ...]}
|
# All deselected goes to exclude with {node_name: [socket1, ...]}
|
||||||
@ -400,7 +587,8 @@ class RT_OT_create_output_layers(bpy.types.Operator):
|
|||||||
excludes=excludes,
|
excludes=excludes,
|
||||||
remap_names=remap_names,
|
remap_names=remap_names,
|
||||||
file_format=file_format,
|
file_format=file_format,
|
||||||
split_tech_passes=self.split_tech_passes)
|
split_tech_passes=self.split_tech_passes,
|
||||||
|
template=self.final_base_path_template)
|
||||||
|
|
||||||
return {"FINISHED"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import bpy
|
import bpy
|
||||||
|
|
||||||
|
from bpy.types import Operator
|
||||||
from bpy.props import (BoolProperty,
|
from bpy.props import (BoolProperty,
|
||||||
EnumProperty,
|
EnumProperty,
|
||||||
PointerProperty,
|
PointerProperty,
|
||||||
@ -8,7 +9,7 @@ from bpy.props import (BoolProperty,
|
|||||||
|
|
||||||
from .. import fn
|
from .. import fn
|
||||||
|
|
||||||
class RT_OT_select_object_by_name(bpy.types.Operator):
|
class RT_OT_select_object_by_name(Operator):
|
||||||
bl_idname = "rt.select_object_by_name"
|
bl_idname = "rt.select_object_by_name"
|
||||||
bl_label = "Select Object By name"
|
bl_label = "Select Object By name"
|
||||||
bl_description = "Select object and modifier"
|
bl_description = "Select object and modifier"
|
||||||
@ -41,6 +42,7 @@ class RT_OT_select_object_by_name(bpy.types.Operator):
|
|||||||
return {'CANCELLED'}
|
return {'CANCELLED'}
|
||||||
|
|
||||||
# Make it the active object
|
# Make it the active object
|
||||||
|
## function to make active and selected
|
||||||
# fn.show_and_active_object(context, target_object, make_active=True, )
|
# fn.show_and_active_object(context, target_object, make_active=True, )
|
||||||
context.view_layer.objects.active = target_object
|
context.view_layer.objects.active = target_object
|
||||||
|
|
||||||
@ -64,8 +66,44 @@ class RT_OT_select_object_by_name(bpy.types.Operator):
|
|||||||
return {'FINISHED'}
|
return {'FINISHED'}
|
||||||
|
|
||||||
|
|
||||||
|
class RT_OT_info_note(Operator):
|
||||||
|
bl_idname = "rt.info_note"
|
||||||
|
bl_label = "Info Note"
|
||||||
|
bl_description = "Info Note"
|
||||||
|
bl_options = {"REGISTER", "INTERNAL"}
|
||||||
|
|
||||||
|
text : bpy.props.StringProperty(default='', options={'SKIP_SAVE'})
|
||||||
|
title : bpy.props.StringProperty(default='Help', options={'SKIP_SAVE'})
|
||||||
|
icon : bpy.props.StringProperty(default='INFO', options={'SKIP_SAVE'})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def description(self, context, properties):
|
||||||
|
return properties.text
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
## Split text in list of lines to display
|
||||||
|
lines = self.text.split('\n')
|
||||||
|
fn.show_message_box(_message=lines, _title=self.title, _icon=self.icon)
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
# Not registered yet
|
||||||
|
class RT_set_env_settings(bpy.types.Operator):
|
||||||
|
"""manually reset environnement settings"""
|
||||||
|
bl_idname = "rt.set_env_settings"
|
||||||
|
bl_label = "Reset Templates From Env"
|
||||||
|
bl_description = "Reset Render Toolbox templates from environment variables"
|
||||||
|
bl_options = {"REGISTER", "UNDO", "INTERNAL"}
|
||||||
|
|
||||||
|
# mode : bpy.props.StringProperty(default='ALL', options={'SKIP_SAVE'}) # 'HIDDEN',
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
from ..properties import set_env_properties
|
||||||
|
set_env_properties()
|
||||||
|
return {'FINISHED'}
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
RT_OT_select_object_by_name,
|
RT_OT_select_object_by_name,
|
||||||
|
RT_OT_info_note
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
86
properties.py
Normal file
86
properties.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import bpy
|
||||||
|
import os
|
||||||
|
|
||||||
|
from bpy.types import PropertyGroup
|
||||||
|
from bpy.props import StringProperty
|
||||||
|
|
||||||
|
|
||||||
|
class RT_PG_render_toolbox_props(PropertyGroup):
|
||||||
|
default_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
|
||||||
|
default_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'}
|
||||||
|
)
|
||||||
|
|
||||||
|
### file slots (not used currently)
|
||||||
|
|
||||||
|
default_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}_",
|
||||||
|
# options={'SKIP_SAVE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
## Multilayer's layer: layer name in EXR instead of subpath to file
|
||||||
|
default_multilayer_name : StringProperty(
|
||||||
|
name="Layer Name Template",
|
||||||
|
description="Template for multilayer file output layer names\
|
||||||
|
\nLayer name in EXR multilayer file",
|
||||||
|
default="{socket_name}",
|
||||||
|
# options={'SKIP_SAVE'}
|
||||||
|
)
|
||||||
|
|
||||||
|
# default_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",
|
||||||
|
# # options={'SKIP_SAVE'}
|
||||||
|
# )
|
||||||
|
|
||||||
|
# default_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",
|
||||||
|
# # options={'SKIP_SAVE'}
|
||||||
|
# )
|
||||||
|
|
||||||
|
def set_env_properties():
|
||||||
|
## set default template from environment variable if available
|
||||||
|
exr_base_path_template = os.getenv('RENDERTOOLBOX_EXR_PATH_TEMPLATE')
|
||||||
|
if exr_base_path_template:
|
||||||
|
bpy.context.scene.render_toolbox.default_base_path = exr_base_path_template
|
||||||
|
|
||||||
|
multilayer_base_path_template = os.getenv('RENDERTOOLBOX_MULTILAYER_PATH_TEMPLATE')
|
||||||
|
if multilayer_base_path_template:
|
||||||
|
bpy.context.scene.render_toolbox.default_multilayer_base_path = multilayer_base_path_template
|
||||||
|
|
||||||
|
classes = (
|
||||||
|
RT_PG_render_toolbox_props,
|
||||||
|
)
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for cls in classes:
|
||||||
|
bpy.utils.register_class(cls)
|
||||||
|
|
||||||
|
bpy.types.Scene.render_toolbox = bpy.props.PointerProperty(type=RT_PG_render_toolbox_props)
|
||||||
|
set_env_properties()
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
del bpy.types.Scene.render_toolbox
|
||||||
|
|
||||||
|
for cls in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(cls)
|
49
ui.py
49
ui.py
@ -3,7 +3,7 @@ import bpy
|
|||||||
from bpy.types import Panel
|
from bpy.types import Panel
|
||||||
|
|
||||||
|
|
||||||
class RT_PT_gp_node_ui(Panel):
|
class RT_PT_render_toolbox_ui(Panel):
|
||||||
bl_space_type = "NODE_EDITOR"
|
bl_space_type = "NODE_EDITOR"
|
||||||
bl_region_type = "UI"
|
bl_region_type = "UI"
|
||||||
bl_category = "Render" # Wrangler
|
bl_category = "Render" # Wrangler
|
||||||
@ -14,8 +14,17 @@ class RT_PT_gp_node_ui(Panel):
|
|||||||
layout.operator("rt.create_output_layers", icon="NODE")
|
layout.operator("rt.create_output_layers", icon="NODE")
|
||||||
layout.operator("rt.outputs_search_and_replace", text='Search and replace outputs', icon="BORDERMOVE")
|
layout.operator("rt.outputs_search_and_replace", text='Search and replace outputs', icon="BORDERMOVE")
|
||||||
|
|
||||||
layout.separator()
|
class RT_PT_visibility_check(Panel):
|
||||||
layout.label(text="Visibily Checks:")
|
bl_space_type = "NODE_EDITOR"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "Render" # Wrangler
|
||||||
|
bl_label = "Visibility Checks"
|
||||||
|
# bl_parent_id = "RT_PT_render_toolbox_ui"
|
||||||
|
# bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
# layout.label(text="Visibily Checks:")
|
||||||
layout.operator("rt.list_object_visibility_conflicts", icon="OBJECT_DATAMODE")
|
layout.operator("rt.list_object_visibility_conflicts", icon="OBJECT_DATAMODE")
|
||||||
layout.operator("rt.list_viewport_render_visibility", text="List Viewport Vs Render Visibility", icon="OBJECT_DATAMODE")
|
layout.operator("rt.list_viewport_render_visibility", text="List Viewport Vs Render Visibility", icon="OBJECT_DATAMODE")
|
||||||
|
|
||||||
@ -23,9 +32,41 @@ class RT_PT_gp_node_ui(Panel):
|
|||||||
|
|
||||||
layout.operator("rt.list_collection_visibility_conflicts", text="List Collections Visibility Conflicts", icon="OUTLINER_COLLECTION")
|
layout.operator("rt.list_collection_visibility_conflicts", text="List Collections Visibility Conflicts", icon="OUTLINER_COLLECTION")
|
||||||
|
|
||||||
|
# layout.separator()
|
||||||
|
# layout.operator("rt.list_object_affected_by_simplify", text="List Object Affected By Simplify", icon="MOD_SIMPLIFY")
|
||||||
|
|
||||||
|
|
||||||
|
## Unused, only exposed in Create output panel
|
||||||
|
class RT_PT_output_template(Panel):
|
||||||
|
bl_space_type = "NODE_EDITOR"
|
||||||
|
bl_region_type = "UI"
|
||||||
|
bl_category = "Render" # Wrangler
|
||||||
|
bl_label = "File Output Templates"
|
||||||
|
bl_parent_id = "RT_PT_render_toolbox_ui"
|
||||||
|
# bl_options = {'DEFAULT_CLOSED'}
|
||||||
|
|
||||||
|
def draw(self, context):
|
||||||
|
layout = self.layout
|
||||||
|
settings = context.scene.render_toolbox
|
||||||
|
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.label(text='Single file:')
|
||||||
|
col.prop(settings, "default_base_path", text="Base Path")
|
||||||
|
col.prop(settings, "default_file_slot", text="File Slot")
|
||||||
|
|
||||||
|
col.separator()
|
||||||
|
col = layout.column(align=True)
|
||||||
|
col.label(text='Multilayers:')
|
||||||
|
col.prop(settings, "default_multilayer_base_path", text="Base Path")
|
||||||
|
col.prop(settings, "default_multilayer_name", text="Layer Name")
|
||||||
|
|
||||||
|
## Handle separate tech passes names ?
|
||||||
|
|
||||||
|
|
||||||
classes = (
|
classes = (
|
||||||
RT_PT_gp_node_ui,
|
RT_PT_render_toolbox_ui,
|
||||||
|
# RT_PT_output_template,
|
||||||
|
RT_PT_visibility_check,
|
||||||
)
|
)
|
||||||
|
|
||||||
def register():
|
def register():
|
||||||
|
Loading…
x
Reference in New Issue
Block a user