diff --git a/__init__.py b/__init__.py index e254139..9dcc6d8 100755 --- a/__init__.py +++ b/__init__.py @@ -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, ) diff --git a/outputs_search_and_replace.py b/outputs_search_and_replace.py new file mode 100644 index 0000000..634e033 --- /dev/null +++ b/outputs_search_and_replace.py @@ -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) \ No newline at end of file diff --git a/setup_outputs.py b/setup_outputs.py index cc2580e..886cfd0 100755 --- a/setup_outputs.py +++ b/setup_outputs.py @@ -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, ) diff --git a/ui.py b/ui.py index 998beba..408aa39 100755 --- a/ui.py +++ b/ui.py @@ -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 = (