import bpy import os import re from . import fn from bpy.props import (StringProperty, BoolProperty, EnumProperty, CollectionProperty) 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" 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: 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: 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_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"} ## ! collection prop -> Now stored on window manager at invoke # socket_collection : CollectionProperty(type=RT_PG_selectable_prop) show_custom_settings : BoolProperty( name='Settings', default=False) split_tech_passes : BoolProperty( name='Separate Tech Passes', default=True, description='Create a separate file output for technical passes (32 bit)') ## enum choice for naming: socket_name, node_name, node_and_socket_name, 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), ('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 : BoolProperty( # name='Prefix With Node Name', # description='Add the node name as prefix to the output name', # default=False) base_path : StringProperty( name='Custom base path', default='', description='Set the base path of created file_output (not if already exists)') file_format : EnumProperty( name='Output Format', default='NONE', items=( ('NONE', 'Default', 'Use default settings'), ('OPEN_EXR_MULTILAYER', 'OpenEXR MultiLayer', 'Output image in multilayer OpenEXR format'), ('OPEN_EXR', 'OpenEXR', 'Output image in OpenEXR format'), ) ) exr_codec : EnumProperty( name='Codec', default='PIZ', description='Codec settings for OpenEXR', items=( ('PIZ', 'PIZ (lossless)', ''), ('ZIP', 'ZIP (lossless)', ''), ('RLE', 'RLE (lossless)', ''), ('ZIPS', 'ZIPS (lossless)', ''), ('PXR24', 'Pxr24 (lossy)', ''), ('B44', 'B44 (lossy)', ''), ('B44A', 'B44A (lossy)', ''), ('DWAA', 'DWAA (lossy)', ''), ('DWAB', 'DWAB (lossy)', ''), ), ) color_depth : EnumProperty( name='Color Depth', default='16', description='Bit depth per channel', items=( # ('8', '8', '8-bit color channels'), # ('10', '10', '10-bit color channels'), # ('12', '12', '12-bit color channels'), ('16', '16 (Half)', '16-bit color channels'), ('32', '32 (Full)', '32-bit color channels'), ), ) 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 self.base_path = '' return self.execute(context) selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE'] if not selected: self.report({'ERROR'}, 'No render layer nodes selected') return {'CANCELLED'} for n in selected: for o in n.outputs: if o.is_unavailable: continue item = self.socket_collection.add() item.node_name = n.name item.node_label = n.label.strip() item.socket_name = o.name ## Set editable names item.name = o.name ## 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}' 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: item.is_linked = True item.select = False return context.window_manager.invoke_props_dialog(self, width=500) def draw(self, context): layout = self.layout box = layout.box() expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT' box.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon) ## Settings if self.show_custom_settings: box.use_property_split = True box.row().prop(self, 'name_type', expand=False) box.prop(self, 'base_path') col = box.column() col.prop(self, 'file_format') col.prop(self, 'exr_codec') col.row().prop(self, 'color_depth', expand=True) col.prop(self, 'split_tech_passes') 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() current_node_name = '' for item in self.socket_collection: if item.node_name != current_node_name: current_node_name = item.node_name col.separator() ## Display node label + name or name only if item.node_label: display_node_name = f'{item.node_label} ({item.node_name})' else: display_node_name = item.node_name ## A bit dirty: For render layer node, show render layer-node name. if display_node_name.startswith('Render Layer'): rln = context.scene.node_tree.nodes.get(item.node_name) display_node_name = f'{rln.scene.name}/{rln.layer} ({display_node_name})' col.label(text=display_node_name, icon='NODE_SEL') row = col.row() if item.is_linked: row.label(text='', icon='LINKED') # NODETREE else: row.label(text='', icon='BLANK1') row.prop(item, 'select', text='') display_name = item.socket_name if 'crypto' in display_name.lower(): display_name = f'{display_name} -> 32bit output node' elif self.split_tech_passes and display_name.lower() in TECH_PASS_KEYWORDS: display_name = f'{display_name} -> Tech pass' row.label(text=display_name) row.label(text='', icon='RIGHTARROW') if self.name_type == 'socket_name': row.prop(item, 'name', text='') elif self.name_type == 'node_name': 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): # Build exclude dict from selection excludes = {} remap_names = {} ## Old system # if len(self.socket_collection): # for item in self.socket_collection: # if not item.select: # # All deselected goes to exclude with {node_name: [socket1, ...]} # excludes.setdefault(item.node_name, []).append(item.name) # elif item.socket_name != item.name: # remap_names[item.socket_name] = item.name if len(self.socket_collection): for item in self.socket_collection: final_name = item.name ## change name if other options were used if self.name_type == 'node_name': 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, ...]} excludes.setdefault(item.node_name, []).append(item.socket_name) elif item.socket_name != final_name: remap_names.setdefault(item.node_name, {})[item.socket_name] = final_name # remap_names[item.socket_name] = final_name ## Handle default file format file_ext = self.file_format if self.file_format == 'NONE': env_file_format = os.environ.get('FILE_FORMAT') file_ext = env_file_format if env_file_format else 'OPEN_EXR_MULTILAYER' file_format = { 'file_format' : file_ext, 'exr_codec' : self.exr_codec, 'color_depth' : self.color_depth, } scn = context.scene nodes = scn.node_tree.nodes selected = [n for n in nodes if n.select] outfile = next((n for n in selected if n.type == 'OUTPUT_FILE'), None) # Exclude output file from selected = [n for n in selected if n.type != 'OUTPUT_FILE'] # fn.connect_to_file_output(selected, outfile) for n in selected: fn.connect_to_file_output( n, outfile, base_path=self.base_path, excludes=excludes, remap_names=remap_names, file_format=file_format, split_tech_passes=self.split_tech_passes) return {"FINISHED"} classes=( RT_OT_search_and_replace, RT_PG_selectable_prop, RT_OT_create_output_layers, ) def register(): for cls in classes: bpy.utils.register_class(cls) def unregister(): for cls in reversed(classes): bpy.utils.unregister_class(cls)