Add search and replace for file outputs

This commit is contained in:
pullusb 2025-07-15 17:12:55 +02:00
parent 2e4a9a3add
commit 9f9284853e
4 changed files with 183 additions and 7 deletions

View File

@ -2,8 +2,8 @@ bl_info = {
"name": "Render Toolbox",
"description": "Perform checks and setup outputs",
"author": "Samuel Bernou",
"version": (0, 3, 1),
"blender": (3, 0, 0),
"version": (0, 4, 0),
"blender": (4, 0, 0),
"location": "View3D",
"warning": "",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox",
@ -12,10 +12,12 @@ bl_info = {
}
from . import setup_outputs
from . import outputs_search_and_replace
from . import ui
bl_modules = (
setup_outputs,
outputs_search_and_replace,
ui,
# prefs,
)

View File

@ -0,0 +1,165 @@
import bpy
import os
import re
from . import fn
from bpy.props import (StringProperty,
BoolProperty,
EnumProperty,
CollectionProperty)
from .constant import TECH_PASS_KEYWORDS
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_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', "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"),
),
default='all'
)
prefix: BoolProperty(name="Prefix Only", description="Affect only prefix of name (skipping names without separator)", default=False)
separator: StringProperty(name="Separator", description="Separator for prefix", default='_')
selected_node_only: BoolProperty(name="Selected Nodes Only", description="Affect only selected file output nodes", default=True)
@classmethod
def poll(cls, context):
return context.scene.node_tree and context.scene.node_tree.nodes
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 invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400)
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, "prefix")
row_b.active = not self.use_regex
subrow_b = row_b.row(align=True)
subrow_b.active = self.prefix
subrow_b.prop(self, "separator")
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 = context.scene.node_tree
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'):
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
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
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)

View File

@ -9,12 +9,15 @@ from bpy.props import (StringProperty,
from .constant import TECH_PASS_KEYWORDS
## -- search and replace (WIP) to batch rename
class RT_OT_search_and_replace(bpy.types.Operator):
bl_idname = "rt.search_and_replace"
# 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="")
@ -103,6 +106,9 @@ class RT_OT_search_and_replace(bpy.types.Operator):
layout.prop(self, "find")
layout.prop(self, "replace")
# endregion
# region Create file output
## -- properties and operator for file output connect
@ -279,7 +285,7 @@ class RT_OT_create_output_layers(bpy.types.Operator):
col.prop(self, 'split_tech_passes')
search_row = layout.row()
op = search_row.operator("rt.search_and_replace", icon='BORDERMOVE')
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'
@ -398,8 +404,10 @@ class RT_OT_create_output_layers(bpy.types.Operator):
return {"FINISHED"}
# endregion
classes=(
RT_OT_search_and_replace,
RT_OT_colprop_search_and_replace,
RT_PG_selectable_prop,
RT_OT_create_output_layers,
)

1
ui.py
View File

@ -12,6 +12,7 @@ class RT_PT_gp_node_ui(Panel):
def draw(self, context):
layout = self.layout
layout.operator("rt.create_output_layers", icon="NODE")
layout.operator("rt.outputs_search_and_replace", text='Search and replace outputs', icon="BORDERMOVE")
classes = (