From 8f689af892ce9eb4daa5c2fa82646d434ae8842d Mon Sep 17 00:00:00 2001 From: pullusb Date: Mon, 31 Mar 2025 17:19:27 +0200 Subject: [PATCH] Add search and replace scene_viewlayers_socket name option 0.2.0 --- __init__.py | 2 +- fn.py | 10 ++- setup_outputs.py | 188 ++++++++++++++++++++++++++++++++++++++++------- ui.py | 2 +- 4 files changed, 169 insertions(+), 33 deletions(-) diff --git a/__init__.py b/__init__.py index 9699d2c..77bc856 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "Render Toolbox", "description": "Perform checks and setup outputs", "author": "Samuel Bernou", - "version": (0, 1, 0), + "version": (0, 2, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index 18c8919..270ab29 100644 --- a/fn.py +++ b/fn.py @@ -105,10 +105,12 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None ## create one output facing node and connect all outs = [o for o in node.outputs if not o.is_unavailable and not 'crypto' in o.name.lower() and o.name not in exclusions] cryptout = [o for o in node.outputs if not o.is_unavailable and 'crypto' in o.name.lower() and o.name not in exclusions] - - if node.type == 'R_LAYERS': - out_base = node.layer - elif node.label: + + + ## Using render layer can force connexion to an existing FO node when user may want a new one + # if node.type == 'R_LAYERS': + # out_base = node.layer + if node.label: out_base = node.label else: out_base = node.name diff --git a/setup_outputs.py b/setup_outputs.py index fd5a355..71dd166 100644 --- a/setup_outputs.py +++ b/setup_outputs.py @@ -1,57 +1,162 @@ import bpy import os +import re from . import fn +from bpy.props import (StringProperty, + BoolProperty, + EnumProperty, + CollectionProperty) + +## -- search and replace (WIP) to batch rename + +class RT_OT_search_and_replace(bpy.types.Operator): + bl_idname = "rt.search_and_replace" + bl_label = "Search And Replace" + bl_description = "Search/Replace texts" + + ## target to affect + data_path: StringProperty(name="Data Path", description="Path to collection prop to affect", default="") + target_prop: StringProperty(name="Target Prop", description="Name of the property to affect in whole collection", default="") + + ## 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') + prefix: BoolProperty(name="Prefix Only", description="Affect only prefix of name (skipping names without separator)", default=False) + use_regex: BoolProperty(name="Regex", description="Use regular expression (advanced), equivalent to python re.sub()", default=False) + + separator: StringProperty(name="Separator", description="Separator to get prefix", default='_') + # selected: BoolProperty(name="Selected Only", description="Affect only selection", default=False) + + 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 execute(self, context): + ## Get the collection prop from data path + collection_prop = eval(self.data_path) + + count = 0 + for item in collection_prop: + ## Get the target prop + name = getattr(item, self.target_prop) + # prop = prop.replace(self.find, self.replace) + new = self.rename(name) + + if new is None or name == new: + continue + + ## Rename in case of difference + print(f'rename: {name} --> {new}') + setattr(item, self.target_prop, new) + count += 1 + + if count: + mess = str(count) + ' rename(s)' + self.report({'INFO'}, mess) + else: + self.report({'WARNING'}, 'Nothing changed') + return{'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + row = layout.row() + # row_a= row.row() + # row_a.prop(self, "separator") + # # row_a.prop(self, "selected") + + row_b= row.row() + row_b.prop(self, "prefix") + row_c= row.row() + + row_c.prop(self, "use_regex") + # row_a.active = not self.use_regex + row_b.active = not self.use_regex + + layout.prop(self, "find") + layout.prop(self, "replace") + + +## -- properties and operator for file output connect class RT_PG_selectable_prop(bpy.types.PropertyGroup): - node_name: bpy.props.StringProperty(name="Node Name") - node_label: bpy.props.StringProperty(name="Node Label") - name: bpy.props.StringProperty(name="Name or Path") - socket_name: bpy.props.StringProperty(name="Source socket Name") # Source socket name as reference - select: bpy.props.BoolProperty(name="Selected", default=True) - is_linked: bpy.props.BoolProperty(name="Linked", default=False) - # is_valid: bpy.props.BoolProperty(name="Valid", default=True) + node_name: StringProperty(name="Node Name") + node_label: StringProperty(name="Node Label") + name: StringProperty(name="Name or Path") + socket_name: StringProperty(name="Source socket Name") # Source socket name as reference + select: BoolProperty(name="Selected", default=True) + is_linked: BoolProperty(name="Linked", default=False) + # is_valid: BoolProperty(name="Valid", default=True) ## extra output naming options - name_from_node: bpy.props.StringProperty(name="Name From Node") - name_from_node_and_socket: bpy.props.StringProperty(name="Name From Node And Socket") + name_from_node: StringProperty(name="Name From Node") + name_from_node_and_socket: StringProperty(name="Name From Node And Socket") + name_from_node_and_socket_with_scene: StringProperty(name="Name From Node And Socket With Scene prefixed") -class RT_OT_connect_selected_to_file_out(bpy.types.Operator): - bl_idname = "rt.connect_selected_to_file_out" - bl_label = "Connect Selected To File Output" - bl_description = "Connect Selected Nodes to a new fileoutput node\ - \nIf a fileoutput node is selected, socket are added to it" +class RT_OT_create_output_layers(bpy.types.Operator): + bl_idname = "rt.create_output_layers" + bl_label = "Create Output Layers" + bl_description = "Connect Selected Nodes to a new file output node\ + \nIf an existing file output node is selected, socket are added to it" bl_options = {"REGISTER", "UNDO"} - socket_collection : bpy.props.CollectionProperty(type=RT_PG_selectable_prop) + ## ! collection prop -> Now stored on window manager at invoke + # socket_collection : CollectionProperty(type=RT_PG_selectable_prop) - show_custom_settings : bpy.props.BoolProperty( + show_custom_settings : BoolProperty( name='Settings', default=False) ## enum choice for naming: socket_name, node_name, node_and_socket_name, - name_type : bpy.props.EnumProperty( + name_type : EnumProperty( name='Output Name From', description='Choose the output name\ \nNode name use Label (Use node name when there is no Label)', default='node_and_socket_name', items=( - ('node_and_socket_name', 'Node_Socket Name', 'Use the node name prefix and socket name', 0), - ('socket_name', 'Socket Name', 'Use the socket name as output name', 1), - ('node_name', 'Node Name', 'Use the node name as output name', 2), + ('node_and_socket_name', 'Node Socket Name', 'Use the node name prefix and socket name', 0), + ('node_and_socket_name_with_scene', 'Scene + Node Socket Name', 'Use the node name prefix and socket name, prefix scene with render layer nodes', 1), + ('socket_name', 'Socket Name', 'Use the socket name as output name', 2), + ('node_name', 'Node Name', 'Use the node name as output name', 3), ) ) - # prefix_with_node_name : bpy.props.BoolProperty( + # prefix_with_node_name : BoolProperty( # name='Prefix With Node Name', # description='Add the node name as prefix to the output name', # default=False) - base_path : bpy.props.StringProperty( + base_path : StringProperty( name='Custom base path', default='', description='Set the base path of created file_output (not if already exists)') - file_format : bpy.props.EnumProperty( + file_format : EnumProperty( name='Output Format', default='NONE', items=( @@ -61,7 +166,7 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): ) ) - exr_codec : bpy.props.EnumProperty( + exr_codec : EnumProperty( name='Codec', default='PIZ', description='Codec settings for OpenEXR', @@ -78,7 +183,7 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): ), ) - color_depth : bpy.props.EnumProperty( + color_depth : EnumProperty( name='Color Depth', default='16', description='Bit depth per channel', @@ -92,6 +197,12 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): ) def invoke(self, context, event): + if not hasattr(context.window_manager, 'rt_socket_collection'): + ## Create collection prop on window manager if not existing + ## (not stored in self anymore, so that other operators can access it, ex: search and replace) + bpy.types.WindowManager.rt_socket_collection = CollectionProperty(type=RT_PG_selectable_prop) + self.socket_collection = context.window_manager.rt_socket_collection + self.socket_collection.clear() if event.ctrl: # Direct connect, do not use any options @@ -116,19 +227,25 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): ## Store other naming options (cleaned at exec with bpy.path.clean_name) node_name = n.label.strip() if n.label.strip() else n.name + scene_node_name = node_name # same by default + + if n.type == 'R_LAYERS': + scene_node_name = f'{n.scene.name}_{n.layer}' ## Change node_name for render layers: scene_viewlayer_name if n.type == 'R_LAYERS' and node_name != n.label: # skip if a label is set node_name = f'{n.layer}' - # node_name = f'{n.scene.name}_{n.layer}' item.name_from_node = node_name if len(n.outputs) == 1: ## Only one output, just pick node name, no need to add socket name item.name_from_node_and_socket = node_name + item.name_from_node_and_socket_with_scene = scene_node_name else: + print(f'node_name: {node_name} VS {scene_node_name}') item.name_from_node_and_socket = f'{node_name}_{o.name}' + item.name_from_node_and_socket_with_scene = f'{scene_node_name}_{o.name}' ## TODO: rename item.name according to template pairs in preferences (to add later) if o.is_linked: @@ -152,6 +269,18 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): col.prop(self, 'exr_codec') col.row().prop(self, 'color_depth', expand=True) + search_row = layout.row() + op = search_row.operator("rt.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' + elif self.name_type == 'node_name': + op.target_prop = 'name_from_node' + elif self.name_type == 'node_and_socket_name': + op.target_prop = 'name_from_node_and_socket' + elif self.name_type == 'node_and_socket_name_with_scene': + op.target_prop = 'name_from_node_and_socket_with_scene' + ## Node Sockets layout.use_property_split = False col = layout.column() @@ -190,6 +319,8 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): row.prop(item, 'name_from_node', text='') elif self.name_type == 'node_and_socket_name': row.prop(item, 'name_from_node_and_socket', text='') + elif self.name_type == 'node_and_socket_name_with_scene': + row.prop(item, 'name_from_node_and_socket_with_scene', text='') def execute(self, context): @@ -213,6 +344,8 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): final_name = item.name_from_node elif self.name_type == 'node_and_socket_name': final_name = item.name_from_node_and_socket + elif self.name_type == 'node_and_socket_name_with_scene': + final_name = item.name_from_node_and_socket_with_scene if not item.select: # All deselected goes to exclude with {node_name: [socket1, ...]} @@ -246,8 +379,9 @@ class RT_OT_connect_selected_to_file_out(bpy.types.Operator): return {"FINISHED"} classes=( +RT_OT_search_and_replace, RT_PG_selectable_prop, -RT_OT_connect_selected_to_file_out, +RT_OT_create_output_layers, ) def register(): diff --git a/ui.py b/ui.py index 9761448..2ac0517 100644 --- a/ui.py +++ b/ui.py @@ -11,7 +11,7 @@ class RT_PT_gp_node_ui(Panel): def draw(self, context): layout = self.layout - layout.operator("rt.connect_selected_to_file_out", icon="NODE") + layout.operator("rt.create_output_layers", icon="NODE") classes = (