- 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
186 lines
6.9 KiB
Python
Executable File
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) |