Files
render_toolbox/operators/output_search_and_replace.py
T
Pullusb d650ec8c98 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
2026-09-29 18:57:26 +02:00

186 lines
6.9 KiB
Python
Executable File

import bpy
import os
import re
from .. import fn
from bpy.props import (StringProperty,
BoolProperty,
EnumProperty,
CollectionProperty)
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 paths (directory and file name) and items"
bl_options = {'REGISTER', 'UNDO'}
## 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')
use_regex: BoolProperty(name="Regex", description="Use regular expression (advanced), equivalent to python re.sub()", default=False)
target : EnumProperty(
name="Target Fields",
description="Fields to search and replace in outputs",
items=(
('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'
)
section : EnumProperty(
name="Section",
description="Search-replace only on a specific section of the text field based on a separator (if no separator exists, skip the field)",
items=(
('full', "Full", "Affect the whole string, No specific section"),
('suffix', "Suffix", "Affect only the part after the last separator"),
('prefix', "Prefix", "Affect only part before the first separator"),
),
default='full'
)
separator: StringProperty(name="Separator", description="Separator for prefix/suffix", default='/')
selected_node_only: BoolProperty(name="Selected Nodes Only", description="Affect only selected file output nodes", default=True)
@classmethod
def poll(cls, context):
tree = fn.get_compositor_tree(context.scene)
return bool(tree and tree.nodes)
def rename(self, source):
if not self.find:
return
old = source
if self.use_regex:
# Directly replace using regex
new = re.sub(self.find, self.replace, source)
if old != new:
return new
## Regex usage exemple to add as hint:
# search (separate in 3 groups) : (.*/)(.*?_)(.*)
# replace (keep only group 1 and 3) : \1\3
# --
# source: "ViewLayer_DiffCol/ViewLayer_DiffCol_"
# result: "ViewLayer_DiffCol/DiffCol_"
return
if self.section == 'full':
new = source.replace(self.find, self.replace)
if old != new:
return new
if not self.separator in source:
# Only if separator exists
return
if self.section == 'prefix':
splited = source.split(self.separator)
prefix = splited[0]
if not prefix:
return
new_prefix = prefix.replace(self.find, self.replace)
if prefix != new_prefix:
splited[0] = new_prefix
return self.separator.join(splited)
elif self.section == 'suffix':
splited = source.rsplit(self.separator, 1)
suffix = splited[-1]
if not suffix:
return
new_suffix = suffix.replace(self.find, self.replace)
if suffix != new_suffix:
splited[-1] = new_suffix
return self.separator.join(splited)
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=430)
def draw(self, context):
layout = self.layout
# col = layout.column(align=False)
# col.prop(self, "separator")
# col.prop(self, "selected_node_only")
row = layout.row(align=False)
row.prop(self, "target", text='Fields', expand=False)
row.prop(self, "selected_node_only", text="Selected File Outputs Only")
row = layout.row()
row_c= row.row()
row_c.prop(self, "use_regex", text="Use Regex")
row_b= layout.row()
row_b.prop(self, "section")
row_b.active = not self.use_regex
subrow_b = row_b.row(align=True)
subrow_b.active = self.section != 'full'
subrow_b.alignment = 'RIGHT'
subrow_b.label(text='Separator:')
subrow_b.prop(self, "separator", text='')
layout.prop(self, "find")
layout.prop(self, "replace")
def execute(self, context):
## Get the collection prop from data path
## any node_tree type (here we specifically want compo nodes)
# node_tree = bpy.context.space_data.edit_tree
# if not node_tree or not node_tree.nodes:
# return
## Checked in poll
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:
file_outputs = [f for f in file_outputs if f.select]
if not file_outputs:
self.report({'ERROR'}, 'No file output nodes found')
return {'CANCELLED'}
print()
count = 0
for file_out in file_outputs:
## Get the target prop
if self.target in ('path', 'all'):
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
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:
self.report({'INFO'}, f"{str(count)} field(s) renamed")
else:
self.report({'WARNING'}, 'Nothing changed')
return{'FINISHED'}
classes = (
RT_OT_outputs_search_and_replace,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)