Update for Blender 5.2

- use new api compositor access
- Fix file output nodes connexion/renumber/rename/slots handlibng
- consider the new output types
- fixes node position with new API
- fix errors with handler

LLM assisted update
This commit is contained in:
Pullusb
2026-09-29 18:57:26 +02:00
parent 02aa67fc26
commit d650ec8c98
12 changed files with 295 additions and 179 deletions
+2
View File
@@ -2,6 +2,8 @@
Blender addon for rendering setup and checks
Requires Blender 5.2+ (for Blender 4.x, use version 1.0 of the addon)
## Environment variable available:
File Output path templates :
+2 -2
View File
@@ -2,8 +2,8 @@ bl_info = {
"name": "Render Toolbox",
"description": "Setup outputs and perform cheks for rendering",
"author": "Samuel Bernou",
"version": (1, 0, 0),
"blender": (4, 0, 0),
"version": (2, 0, 0),
"blender": (5, 2, 0),
"location": "View3D",
"warning": "",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox",
+148 -60
View File
@@ -11,31 +11,84 @@ def get_addon_prefs():
# region Manage nodes
def get_compositor_tree(scene=None):
'''Return the compositing node group of passed scene (active scene if not passed)
None if the scene has no compositing node group
'''
scene = scene or bpy.context.scene
return scene.compositing_node_group
def get_group_output_node(tree, create=False):
'''Return the active group output node of the tree (compositor output since Blender 5.0)
create: create the node, and an "Image" output socket in tree interface, if missing
'''
out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT' and n.is_active_output), None)
if out is None:
out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT'), None)
if not create:
return out
if not any(i.item_type == 'SOCKET' and i.in_out == 'OUTPUT' for i in tree.interface.items_tree):
tree.interface.new_socket(name='Image', in_out='OUTPUT', socket_type='NodeSocketColor')
if out is None:
out = tree.nodes.new('NodeGroupOutput')
return out
def real_loc(n):
if not n.parent:
return n.location
return n.location + real_loc(n.parent)
return n.location_absolute
def get_media_type(file_format):
'''Return the media type corresponding to passed file format'''
if file_format == 'OPEN_EXR_MULTILAYER':
return 'MULTI_LAYER_IMAGE'
if file_format == 'FFMPEG':
return 'VIDEO'
return 'IMAGE'
def set_format_settings(fmt, settings):
'''Apply a dict of attributes to an image format settings (e.g: file output node format)
media_type and file_format are set first, available values of other attributes depend on them
'''
settings = dict(settings)
file_format = settings.pop('file_format', None)
media_type = settings.pop('media_type', None)
if file_format and not media_type:
media_type = get_media_type(file_format)
if media_type:
fmt.media_type = media_type
if file_format:
fmt.file_format = file_format
for k, v in settings.items():
setattr(fmt, k, v)
def set_scene_output_from_active_fileout_item():
scn = bpy.context.scene
rd = scn.render
ntree = scn.node_tree
ntree = get_compositor_tree(scn)
fo = ntree.nodes.active
if fo.type != 'OUTPUT_FILE':
return
sl = fo.file_slots[fo.active_input_index]
full_path = os.path.join(fo.base_path, sl.path)
item = fo.file_output_items[fo.active_item_index]
if fo.format.media_type == 'MULTI_LAYER_IMAGE':
## All items are layers of the same file
full_path = os.path.join(fo.directory, fo.file_name)
fmt = fo.format
else:
full_path = os.path.join(fo.directory, fo.file_name + item.name)
fmt = item.format if item.override_node_format else fo.format
rd.filepath = full_path
fmt = fo.format if sl.use_node_format else sl.format
## set those attr first to avoid error settings other attributes in next loop
rd.image_settings.media_type = fmt.media_type
rd.image_settings.file_format = fmt.file_format
rd.image_settings.color_mode = fmt.color_mode
rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else 8 # Force set since Sometimes it's weirdly set to "" (not in enum choice)
rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else '8' # Force set since Sometimes it's weirdly set to "" (not in enum choice)
excluded = ['file_format', 'color_mode', 'color_depth',
excluded = ['media_type', 'file_format', 'color_mode', 'color_depth',
'view_settings', 'views_format']
''' ## all attrs
@@ -68,21 +121,61 @@ def set_file_output_format(fo):
'color_mode': 'RGBA'
}
for k, v in env_file_format.items():
setattr(fo.format, k, v)
set_format_settings(fo.format, env_file_format)
def set_file_output_path(fo, path, suffix=''):
'''Set directory and file name of a file output node from a single path
Multilayer: last element is the file name, ex: "//render/name/name_"
Other formats: whole path is the directory, ex: "//render/name/"
suffix: optional subfolder added at the end of the directory, ex: "tech/"
'''
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
directory, file_name = re.match(r'(.*[\\/])?(.*)', path).groups()
directory = directory or ''
else:
directory, file_name = path, ''
if suffix:
directory = directory.rstrip('\\/') + '/' + suffix if directory else suffix
fo.directory = directory
fo.file_name = file_name
## Source socket type to file output item socket type
FILE_OUTPUT_ITEM_TYPES = {
'VALUE': 'FLOAT',
'INT': 'INT',
'BOOLEAN': 'BOOLEAN',
'VECTOR': 'VECTOR',
'RGBA': 'RGBA',
}
def add_file_output_item(fo, name, socket=None):
'''Add an item to file output node
socket: source socket, if passed item type (and vector dimensions) match the socket
return created item and its input socket
'''
socket_type = FILE_OUTPUT_ITEM_TYPES.get(socket.type, 'RGBA') if socket is not None else 'RGBA'
item = fo.file_output_items.new(socket_type, name)
if socket_type == 'VECTOR' and hasattr(socket, 'default_value'):
## ex: render layer "Vector" pass is 4D
item.vector_socket_dimensions = len(socket.default_value)
## items inputs are in the same order (followed by a virtual socket)
return item, fo.inputs[len(fo.file_output_items) - 1]
def clear_disconnected(fo):
'''Remove unlinked inputs from file output node'''
for inp in reversed(fo.inputs):
if not inp.is_linked:
print(f'Deleting unlinked fileout slot: {inp.name}')
fo.inputs.remove(inp)
'''Remove unlinked items from file output node'''
items = fo.file_output_items
for i in reversed(range(len(items))):
if not fo.inputs[i].is_linked:
print(f'Deleting unlinked fileout item: {items[i].name}')
items.remove(items[i])
def create_node(type, tree=None, **kargs):
'''Get a type, a tree to add in, and optionnaly multiple attribute to set
return created node
'''
tree = tree or bpy.context.scene.node_tree
tree = tree or get_compositor_tree()
node = tree.nodes.new(type)
for k,v in kargs.items():
@@ -130,21 +223,19 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
if not outputs:
return None
nodes = scene.node_tree.nodes
links = scene.node_tree.links
tree = get_compositor_tree(scene)
nodes = tree.nodes
links = tree.links
fo = file_out
if not fo:
fo = nodes.get(out_name)
if not fo:
fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree,
location=(real_loc(node)[0] + location_offset[0],
real_loc(node)[1] + location_offset[1]), width=600)
fo.inputs.remove(fo.inputs[0]) # Remove default image input
fo = create_node('CompositorNodeOutputFile', tree=tree, width=600)
fo.file_output_items.clear() # Ensure there is no default item
if file_format:
for k, v in file_format.items():
setattr(fo.format, k, v)
set_format_settings(fo.format, file_format)
else:
set_file_output_format(fo)
@@ -154,32 +245,26 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
fo.name = out_name
if node.parent:
fo.parent = node.parent
fo.location_absolute = (real_loc(node)[0] + location_offset[0],
real_loc(node)[1] + location_offset[1])
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:
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,
)
elif base_path:
path = base_path
elif fo.format.file_format == 'OPEN_EXR_MULTILAYER':
# default behavior without template
if base_path:
fo.base_path = base_path
else:
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
fo.base_path = f'//render/{out_base}/{suffix}{out_base}_'
else:
fo.base_path = f'//render/{out_base}/{suffix}'
path = f'//render/{out_base}/{out_base}_'
else:
path = f'//render/{out_base}/'
## suffix is added as a subfolder of the directory
set_file_output_path(fo, path, suffix=suffix)
for o in outputs:
if next((l for l in o.links if recursive_node_connect_check(l, fo)), None):
@@ -190,18 +275,15 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path,
else:
slot_name = bpy.path.clean_name(o.name)
fs = fo.file_slots.new('tmp')
ls = fo.layer_slots.new('tmp')
## Multilayer: item name is the layer name in the file
## Other formats: item name is the subpath added to directory + file name
if fo.format.file_format == 'OPEN_EXR_MULTILAYER' or use_slot_template:
## use_slot_template always False (currently never use slot template)
item_name = slot_name
else:
item_name = f'{slot_name}/{slot_name}_'
ls = fo.layer_slots[-1]
ls.name = slot_name
fs = fo.file_slots[-1]
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]
_item, out_input = add_file_output_item(fo, item_name, socket=o)
links.new(o, out_input)
clear_disconnected(fo)
@@ -451,6 +533,12 @@ def set_properties_editor_tab(tab, skip_if_exists=True):
return 1
def get_object_icon(obj):
'''Return outliner icon name of passed object'''
if obj.type == 'LIGHT_PROBE':
return 'OUTLINER_OB_LIGHTPROBE'
return f'OUTLINER_OB_{obj.type}'
def show_and_active_object(obj, make_active=True, select=True, unhide=True):
'''
Show the object
@@ -685,7 +773,7 @@ def store_visibility_states(collection=None):
def store_collection_states(collection, context=None):
'''Pass a colelction and store visibility state'''
'''Pass a collection and store visibility state'''
context = context or bpy.context
scene = context.scene
+1 -1
View File
@@ -244,7 +244,7 @@ class RT_OT_conform_collection_hierarchy(Operator):
for k, v in type_dict.items():
if item_type == "objects":
icon = f'OUTLINER_OB_{k.type}'
icon = fn.get_object_icon(k)
name = k.name
else:
icon = 'OUTLINER_COLLECTION'
+65 -62
View File
@@ -11,24 +11,16 @@ from .. import fn
# region renumber outputs
def delete_numbering(fo): # padding=3
'''Delete prefix numbering on all slots on passed file output'''
'''Delete prefix numbering on all items on passed file output'''
if fo.type != 'OUTPUT_FILE': return
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
slots = fo.layer_slots
field_attr = 'name'
else:
slots = fo.file_slots
field_attr = 'path'
for fs in slots:
elems = getattr(fs, field_attr).split('/')
for fs in fo.file_output_items:
elems = fs.name.split('/')
for i, e in enumerate(elems):
elems[i] = re.sub(r'^\d{3}_', '', e)
new = '/'.join(elems)
setattr(fs, field_attr, new)
fs.name = '/'.join(elems)
## Add - Insert prefix incrementation in fileoutput
@@ -43,9 +35,7 @@ def get_numbered_output(out, slot_name):
def add_fileslot_number(fs, number):
field_attr = 'name' if hasattr(fs, 'name') else 'path'
elems = getattr(fs, field_attr).split('/')
elems = fs.name.split('/')
for i, e in enumerate(elems):
if re.match(r'^\d{3}_', e):
elems[i] = re.sub(r'^(\d{3})', lambda x: str(number).zfill(3), e)
@@ -53,37 +43,34 @@ def add_fileslot_number(fs, number):
elems[i] = f'{str(number).zfill(3)}_{e}'
new = '/'.join(elems)
setattr(fs, field_attr, new)
fs.name = new
return new
def renumber(fo, offset=10):
'''Force renumber all the slots with a 3'''
'''Force renumber all the items with a 3'''
if fo.type != 'OUTPUT_FILE': return
ct = 10 # start at 10
slots = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots
for fs in slots:
for fs in fo.file_output_items:
add_fileslot_number(fs, ct)
ct += offset
def get_num(string) -> int:
'''get a tring or a file_slot object
'''get a string or a file output item object
return leading number or None
'''
if not isinstance(string, str):
if hasattr(string, 'path'):
string = string.path
else:
string = string.name
string = string.name
num = re.search(r'^(\d{3})_', string)
if num:
return int(num.group(1))
def reverse_fileout_inputs(fo):
count = len(fo.inputs)
items = fo.file_output_items
count = len(items)
for i in range(count):
fo.inputs.move(count-1, i)
items.move(count-1, i)
def renumber_keep_existing(fo, offset=10, invert=True):
'''Renumber by keeping existing numbers and inserting new one whenever possible
@@ -96,7 +83,7 @@ def renumber_keep_existing(fo, offset=10, invert=True):
if invert:
reverse_fileout_inputs(fo)
fsl = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots
fsl = fo.file_output_items
last_idx = len(fsl) - 1
prev = None
@@ -140,7 +127,7 @@ def renumber_keep_existing(fo, offset=10, invert=True):
if next_num is None:
add_fileslot_number(fs, 0)
elif next_num == 0:
print(f'Cannot insert value before 0 to {fsl.path}')
print(f'Cannot insert value before 0 to {fs.name}')
continue
else:
add_fileslot_number(fs, int(next_num / 2))
@@ -161,7 +148,7 @@ def renumber_keep_existing(fo, offset=10, invert=True):
# iterate rename
gap_inc = int((next_num - prev_num) / divider)
if gap_inc < 1: # same values !
print(f'cannot insert a median value at {fs.path} between {prev_num} and {next_num}')
print(f'cannot insert a median value at {fs.name} between {prev_num} and {next_num}')
continue
ct = prev_num
@@ -187,7 +174,7 @@ class RT_OT_number_outputs(Operator):
@classmethod
def poll(cls, context):
return True
return fn.get_compositor_tree(context.scene) is not None
mode : StringProperty(default='SELECTED', options={'SKIP_SAVE'})
clear : BoolProperty(default=False, options={'SKIP_SAVE'})
@@ -199,10 +186,8 @@ class RT_OT_number_outputs(Operator):
return self.execute(context)
def execute(self, context):
scn = context.scene
ct = 0
nodes = scn.node_tree.nodes
nodes = fn.get_compositor_tree(context.scene).nodes
for fo in nodes:
if fo.type != 'OUTPUT_FILE':
continue
@@ -234,9 +219,13 @@ class RT_OT_mute_toggle_output_nodes(Operator):
mute : BoolProperty(default=True, options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return fn.get_compositor_tree(context.scene) is not None
def execute(self, context):
ct = 0
for n in context.scene.node_tree.nodes:
for n in fn.get_compositor_tree(context.scene).nodes:
if n.type != 'OUTPUT_FILE':
continue
n.mute = self.mute
@@ -255,8 +244,12 @@ class RT_OT_set_output_node_format(Operator):
mute : BoolProperty(default=True, options={'SKIP_SAVE'})
@classmethod
def poll(cls, context):
return fn.get_compositor_tree(context.scene) is not None
def execute(self, context):
nodes = context.scene.node_tree.nodes
nodes = fn.get_compositor_tree(context.scene).nodes
if not nodes.active or nodes.active.type != 'OUTPUT_FILE':
self.report({"ERROR"}, f'Active node should be an output file to use as reference for output format')
return {"CANCELLED"}
@@ -273,8 +266,14 @@ class RT_OT_set_output_node_format(Operator):
if n.type != 'OUTPUT_FILE' or n == ref or not n.select:
continue
## media type and file format first, available values of other attributes depend on them
n.format.media_type = ref.format.media_type
n.format.file_format = ref.format.file_format
for attr in dir(ref.format):
if attr.startswith('__') or attr in {'rna_type','bl_rna', 'view_settings', 'display_settings','stereo_3d_format'}: # views_format
if attr.startswith('__') or attr in {'rna_type','bl_rna', 'media_type', 'file_format',
'view_settings', 'display_settings', 'linear_colorspace_settings',
'has_linear_colorspace', 'stereo_3d_format'}: # views_format
continue
try:
setattr(n.format, attr, getattr(ref.format, attr))
@@ -295,36 +294,38 @@ class RT_OT_set_output_node_format(Operator):
class RT_OT_set_active_file_output_slot_to_composite(bpy.types.Operator):
bl_idname = "rt.set_active_file_output_slot_to_composite"
bl_label = "Set Active File Output Slot To Composite"
bl_description = "Use active slot of active file output node to set scene output settings (swap connection)"
bl_label = "Set Active File Output Item To Composite"
bl_description = "Use active item of active file output node to set scene output settings\
\nand swap connection to compositor Group Output node"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.scene.use_nodes\
and context.scene.node_tree\
and context.scene.node_tree.nodes.active\
and context.scene.node_tree.nodes.active.type == 'OUTPUT_FILE'
tree = fn.get_compositor_tree(context.scene)
return bool(tree\
and tree.nodes.active\
and tree.nodes.active.type == 'OUTPUT_FILE')
relink_composite : bpy.props.BoolProperty(
name='Relink Composite',
default=True,
description='In case file slot is linked, swap link to Composite file',
description='In case file output item is linked, swap link to compositor Group Output',
options={'SKIP_SAVE'},
)
def invoke(self, context, event):
self.fo = context.scene.node_tree.nodes.active
if not len(self.fo.file_slots):
self.report({'ERROR'}, 'no slots in active file output')
tree = fn.get_compositor_tree(context.scene)
self.fo = tree.nodes.active
if not len(self.fo.file_output_items):
self.report({'ERROR'}, 'no items in active file output')
return {'CANCELLED'}
# check if active slot has a source
if not self.fo.inputs[self.fo.active_input_index].is_linked:
# check if active item has a source
if not self.fo.inputs[self.fo.active_item_index].is_linked:
return self.execute(context)
# check if composite linked
out = context.scene.node_tree.nodes.get('Composite')
out = fn.get_group_output_node(tree)
if not out or not out.inputs[0].is_linked:
self.compo_out_from_link = ''
return self.execute(context)
@@ -337,34 +338,36 @@ class RT_OT_set_active_file_output_slot_to_composite(bpy.types.Operator):
def draw(self, context):
layout = self.layout
col = layout.column()
col.label(text=f'Composite node connected to: {self.compo_out_from_link}')
col.label(text=f'Would you like to replace by file output slot source ?')
col.label(text=f'Group Output node connected to: {self.compo_out_from_link}')
col.label(text=f'Would you like to replace by file output item source ?')
layout.prop(self, 'relink_composite')
def execute(self, context):
self.fo = fn.get_compositor_tree(context.scene).nodes.active
if not len(self.fo.file_output_items):
self.report({'ERROR'}, 'no items in active file output')
return {'CANCELLED'}
# if comp
fn.set_scene_output_from_active_fileout_item()
idx = self.fo.active_input_index
sl = self.fo.file_slots[idx]
idx = self.fo.active_item_index
item = self.fo.file_output_items[idx]
sk = self.fo.inputs[idx]
if not sk.is_linked:
self.report({'INFO'}, f'Outut changed to match {sl.path} (slot was not linked)')
self.report({'INFO'}, f'Outut changed to match {item.name} (item was not linked)')
return {'FINISHED'}
## If linked replace links to Composite node
## If linked replace links to Group Output node
if not self.relink_composite:
return {'FINISHED'}
ntree = context.scene.node_tree
links = context.scene.node_tree.links
nodes = context.scene.node_tree.nodes
ntree = fn.get_compositor_tree(context.scene)
links = ntree.links
out = nodes.get('Composite')
if not out:
out = fn.create_node('COMPOSITE', tree=ntree)
out = fn.get_group_output_node(ntree, create=True)
fo_loc = fn.real_loc(self.fo)
out.location = (fo_loc.x, fo_loc.y + 160)
out.location_absolute = (fo_loc.x, fo_loc.y + 160)
# if out.inputs[0].is_linked:
# self.report({'WARNING'}, f'Outut changed to match {sl.path} (Composite node already linked)')
+20 -27
View File
@@ -10,7 +10,7 @@ from bpy.props import (StringProperty,
class RT_OT_outputs_search_and_replace(bpy.types.Operator):
bl_idname = "rt.outputs_search_and_replace"
bl_label = "Search And Replace Outputs Paths"
bl_description = "Search/Replace texts in output path and slots"
bl_description = "Search/Replace texts in output paths (directory and file name) and items"
bl_options = {'REGISTER', 'UNDO'}
## Search and replace options
@@ -22,9 +22,9 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
name="Target Fields",
description="Fields to search and replace in outputs",
items=(
('path', "Base Paths", "search and replace in output node paths"),
('slots', "File Slots", "search and replace in output node file slots (also Layer slots for multilayers outputs)"),
('all', "All", "search and replace in both paths and slots"),
('path', "Paths", "search and replace in output node paths (directory and file name, separately)"),
('slots', "Items", "search and replace in output node items (subpaths, or layer names for multilayers outputs)"),
('all', "All", "search and replace in both paths and items"),
),
default='all'
)
@@ -46,7 +46,8 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
@classmethod
def poll(cls, context):
return context.scene.node_tree and context.scene.node_tree.nodes
tree = fn.get_compositor_tree(context.scene)
return bool(tree and tree.nodes)
def rename(self, source):
if not self.find:
@@ -133,7 +134,7 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
# return
## Checked in poll
node_tree = context.scene.node_tree
node_tree = fn.get_compositor_tree(context.scene)
file_outputs = [f for f in node_tree.nodes if f.type == 'OUTPUT_FILE']
if self.selected_node_only:
@@ -148,29 +149,21 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator):
for file_out in file_outputs:
## Get the target prop
if self.target in ('path', 'all'):
current_base_path = file_out.base_path
new_base_path = self.rename(current_base_path)
if new_base_path is not None and current_base_path != new_base_path:
print(f"\n{file_out.name}: base path: {current_base_path} to {new_base_path}")
file_out.base_path = new_base_path
count += 1
if self.target in ('slots', 'all'):
for slot in file_out.file_slots:
current_slot_path = slot.path
new_slot_path = self.rename(current_slot_path)
if new_slot_path is not None and current_slot_path != new_slot_path:
print(f"{file_out.name}: file slot: {current_slot_path} to {new_slot_path}")
slot.path = new_slot_path
for attr in ('directory', 'file_name'):
current_path = getattr(file_out, attr)
new_path = self.rename(current_path)
if new_path is not None and current_path != new_path:
print(f"\n{file_out.name}: {attr}: {current_path} to {new_path}")
setattr(file_out, attr, new_path)
count += 1
## Layers slots
for layer in file_out.layer_slots:
current_layer_path = layer.name
new_layer_path = self.rename(current_layer_path)
if new_layer_path is not None and current_layer_path != new_layer_path:
print(f"{file_out.name}: layer slot: {current_layer_path} to {new_layer_path}")
layer.name = new_layer_path
if self.target in ('slots', 'all'):
for item in file_out.file_output_items:
current_item_name = item.name
new_item_name = self.rename(current_item_name)
if new_item_name is not None and current_item_name != new_item_name:
print(f"{file_out.name}: item: {current_item_name} to {new_item_name}")
item.name = new_item_name
count += 1
if count:
+23 -5
View File
@@ -324,9 +324,16 @@ class RT_OT_create_output_layers(bpy.types.Operator):
self.socket_collection = context.window_manager.rt_socket_collection
self.socket_collection.clear()
tree = fn.get_compositor_tree(context.scene)
if tree is None:
self.report({'ERROR'}, 'Scene has no compositing node group')
return {'CANCELLED'}
if event.ctrl:
# Direct connect, do not use any options
self.base_path = ''
self.final_base_path_template = None
return self.execute(context)
## Store variable usable in templates
@@ -344,7 +351,7 @@ class RT_OT_create_output_layers(bpy.types.Operator):
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']
selected = [n for n in tree.nodes if n.select and n.type != 'OUTPUT_FILE']
if not selected:
self.report({'ERROR'}, 'No render layer nodes selected')
return {'CANCELLED'}
@@ -411,9 +418,21 @@ class RT_OT_create_output_layers(bpy.types.Operator):
if ref.type == 'R_LAYERS':
self.scene_name = ref.scene.name
self.view_layer_name = ref.layer
self.final_base_path_template = ''
## Set here too, since draw only update it when settings are expanded
self.final_base_path_template = self.get_base_path_template(context)
return context.window_manager.invoke_props_dialog(self, width=530)
def get_base_path_template(self, context):
'''Return base path template from scene settings, None if templates are disabled'''
if not self.use_base_path_templates:
return None
final_format = self.file_format
if final_format == 'NONE':
final_format = os.environ.get('FILE_FORMAT', 'OPEN_EXR_MULTILAYER')
if final_format == 'OPEN_EXR_MULTILAYER':
return context.scene.render_toolbox.default_multilayer_base_path
return context.scene.render_toolbox.default_base_path
def draw(self, context):
layout = self.layout
box = layout.box()
@@ -530,7 +549,7 @@ Possible variables:
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)
rln = fn.get_compositor_tree(context.scene).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')
@@ -623,8 +642,7 @@ Possible variables:
'color_depth' : '32',
}
scn = context.scene
nodes = scn.node_tree.nodes
nodes = fn.get_compositor_tree(context.scene).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
+4 -2
View File
@@ -7,6 +7,8 @@ from bpy.props import (BoolProperty,
CollectionProperty,
StringProperty)
from .. import fn
def list_simplify_affected_objects(context):
"""Return a list of object affected by simplify
return : [[obj, [{type: "SUBSURF", ..}, {..}], icon], ..]
@@ -103,7 +105,7 @@ def list_simplify_affected_objects(context):
# If object has affected data, add it to the list
if affected_data:
ob_list.append([obj, affected_data, "OUTLINER_OB_" + obj.type])
ob_list.append([obj, affected_data, fn.get_object_icon(obj)])
return ob_list
@@ -288,7 +290,7 @@ class RT_OT_list_object_affected_by_simplify(Operator):
# Create subrow for hair info
subrow = row.row()
subrow.label(text=f"Hair: {mod.name}", icon='HAIR')
subrow.label(text=f"Hair: {mod.name}", icon='CURVES_DATA')
subrow.label(text=f"Node Group: {data['node_group']}")
elif data['type'] == 'SUBSURFACE':
+7 -1
View File
@@ -5,6 +5,8 @@ from bpy.props import (StringProperty,
BoolProperty,
)
from .. import fn
class RT_OT_enable_all_viewlayers(Operator):
bl_idname = "rt.enable_all_viewlayers"
bl_label = "Enable All Viewlayers"
@@ -27,10 +29,14 @@ class RT_OT_activate_only_selected_layers(Operator):
bl_description = "Activate only selected node view layer , excluding all others"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return fn.get_compositor_tree(context.scene) is not None
def execute(self, context):
scn = context.scene
nodes = scn.node_tree.nodes
nodes = fn.get_compositor_tree(scn).nodes
rlayers_nodes = [n for n in nodes if n.select and n.type == 'R_LAYERS']
vls = [scn.view_layers.get(n.layer) for n in rlayers_nodes if scn.view_layers.get(n.layer)]
+1 -1
View File
@@ -425,7 +425,7 @@ class RT_OT_list_modifier_visibility(Operator):
for m in o.modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
self.ob_list.append([o, mods, fn.get_object_icon(o)])
mods.append(m)
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
return context.window_manager.invoke_props_dialog(self, width=350)
+1 -1
View File
@@ -133,7 +133,7 @@ def register():
bpy.app.handlers.load_post.append(set_render_toolbox_settings)
def unregister():
if not 'set_render_toolbox_settings' in [hand.__name__ for hand in bpy.app.handlers.load_post]:
if 'set_render_toolbox_settings' in [hand.__name__ for hand in bpy.app.handlers.load_post]:
bpy.app.handlers.load_post.remove(set_render_toolbox_settings)
for cls in reversed(classes):
+12 -8
View File
@@ -68,14 +68,15 @@ class RT_PT_render_toolbox_ui(Panel):
col.prop(scn.render, 'resolution_percentage', text="Resolution Percentage")
percent = scn.render.resolution_percentage
col.label(text=f"{int(scn.render.resolution_x * percent / 100)}x{int(scn.render.resolution_y * percent / 100)}", icon='INFO')
if cam and cam.data.shift_x != 0 or cam.data.shift_y != 0:
if cam and (cam.data.shift_x != 0 or cam.data.shift_y != 0):
col.label(text='Camera has Shift', icon='INFO')
# col.prop(cam.data, 'shift_x', text="Shift X")
# col.prop(cam.data, 'shift_y', text="Shift Y")
## viewlayer section
layout.label(text='View layers:')
ct = len([n for n in context.scene.node_tree.nodes if n.type == 'R_LAYERS' and n.select])
tree = fn.get_compositor_tree(scn)
ct = len([n for n in tree.nodes if n.type == 'R_LAYERS' and n.select]) if tree else 0
# col = layout.column(align=True)
# row=col.row(align=True)
@@ -109,13 +110,18 @@ class RT_PT_file_output_ui(bpy.types.Panel):
def draw(self, context):
layout = self.layout
tree = fn.get_compositor_tree(context.scene)
if tree is None:
layout.label(text='Scene has no compositing node group', icon='INFO')
return
col = layout.column()
col.operator("rt.create_output_layers", text='Create File Output', icon="NODE")
col.operator("rt.outputs_search_and_replace", text='Search And Replace Outputs', icon="BORDERMOVE")
col.separator()
col.operator('rt.set_output_node_format', icon='OUTPUT', text='Copy Output Format To Selected')
col.operator('rt.set_active_file_output_slot_to_composite', icon='OUTPUT', text='Active Slot To Composite')
col.operator('rt.set_active_file_output_slot_to_composite', icon='OUTPUT', text='Active Item To Composite')
layout.label(text='All Outputs:')
@@ -123,17 +129,15 @@ class RT_PT_file_output_ui(bpy.types.Panel):
row.operator('rt.mute_toggle_output_nodes', icon='NODE_INSERT_ON', text='Mute').mute = True
row.operator('rt.mute_toggle_output_nodes', icon='NODE_INSERT_OFF', text='Unmute').mute = False
scn = context.scene
disabled_output = [n for n in scn.node_tree.nodes if n.type == 'OUTPUT_FILE' and n.mute]
disabled_output = [n for n in tree.nodes if n.type == 'OUTPUT_FILE' and n.mute]
if disabled_output:
output_ct = len([n for n in scn.node_tree.nodes if n.type == 'OUTPUT_FILE'])
output_ct = len([n for n in tree.nodes if n.type == 'OUTPUT_FILE'])
layout.label(text=f'{len(disabled_output)}/{output_ct} Output Muted', icon='INFO')
col = layout.column()
## (re)number exports
ct = len([n for n in context.scene.node_tree.nodes if n.type == 'OUTPUT_FILE' and n.select])
ct = len([n for n in tree.nodes if n.type == 'OUTPUT_FILE' and n.select])
txt = f'Renumber {ct} Selected Outputs'
subcol = col.column()
subcol.enabled = bool(ct)