diff --git a/README.md b/README.md index 4d29419..91865be 100755 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Blender addon for rendering setup and checks +Requires Blender 5.2+ (for Blender 4.x, use version 1.0 of the addon) + ## Environment variable available: File Output path templates : diff --git a/__init__.py b/__init__.py index b247234..4b7600e 100755 --- a/__init__.py +++ b/__init__.py @@ -2,8 +2,8 @@ bl_info = { "name": "Render Toolbox", "description": "Setup outputs and perform cheks for rendering", "author": "Samuel Bernou", - "version": (1, 0, 0), - "blender": (4, 0, 0), + "version": (2, 0, 0), + "blender": (5, 2, 0), "location": "View3D", "warning": "", "doc_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox", diff --git a/fn.py b/fn.py index 1b61f5f..406779f 100755 --- a/fn.py +++ b/fn.py @@ -11,31 +11,84 @@ def get_addon_prefs(): # region Manage nodes +def get_compositor_tree(scene=None): + '''Return the compositing node group of passed scene (active scene if not passed) + None if the scene has no compositing node group + ''' + scene = scene or bpy.context.scene + return scene.compositing_node_group + +def get_group_output_node(tree, create=False): + '''Return the active group output node of the tree (compositor output since Blender 5.0) + create: create the node, and an "Image" output socket in tree interface, if missing + ''' + out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT' and n.is_active_output), None) + if out is None: + out = next((n for n in tree.nodes if n.type == 'GROUP_OUTPUT'), None) + if not create: + return out + + if not any(i.item_type == 'SOCKET' and i.in_out == 'OUTPUT' for i in tree.interface.items_tree): + tree.interface.new_socket(name='Image', in_out='OUTPUT', socket_type='NodeSocketColor') + if out is None: + out = tree.nodes.new('NodeGroupOutput') + return out + def real_loc(n): - if not n.parent: - return n.location - return n.location + real_loc(n.parent) + return n.location_absolute + +def get_media_type(file_format): + '''Return the media type corresponding to passed file format''' + if file_format == 'OPEN_EXR_MULTILAYER': + return 'MULTI_LAYER_IMAGE' + if file_format == 'FFMPEG': + return 'VIDEO' + return 'IMAGE' + +def set_format_settings(fmt, settings): + '''Apply a dict of attributes to an image format settings (e.g: file output node format) + media_type and file_format are set first, available values of other attributes depend on them + ''' + settings = dict(settings) + file_format = settings.pop('file_format', None) + media_type = settings.pop('media_type', None) + if file_format and not media_type: + media_type = get_media_type(file_format) + + if media_type: + fmt.media_type = media_type + if file_format: + fmt.file_format = file_format + for k, v in settings.items(): + setattr(fmt, k, v) def set_scene_output_from_active_fileout_item(): scn = bpy.context.scene rd = scn.render - ntree = scn.node_tree + ntree = get_compositor_tree(scn) fo = ntree.nodes.active if fo.type != 'OUTPUT_FILE': return - sl = fo.file_slots[fo.active_input_index] - full_path = os.path.join(fo.base_path, sl.path) + item = fo.file_output_items[fo.active_item_index] + + if fo.format.media_type == 'MULTI_LAYER_IMAGE': + ## All items are layers of the same file + full_path = os.path.join(fo.directory, fo.file_name) + fmt = fo.format + else: + full_path = os.path.join(fo.directory, fo.file_name + item.name) + fmt = item.format if item.override_node_format else fo.format rd.filepath = full_path - fmt = fo.format if sl.use_node_format else sl.format ## set those attr first to avoid error settings other attributes in next loop + rd.image_settings.media_type = fmt.media_type rd.image_settings.file_format = fmt.file_format rd.image_settings.color_mode = fmt.color_mode - rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else 8 # Force set since Sometimes it's weirdly set to "" (not in enum choice) + rd.image_settings.color_depth = fmt.color_depth if fmt.color_depth else '8' # Force set since Sometimes it's weirdly set to "" (not in enum choice) - excluded = ['file_format', 'color_mode', 'color_depth', + excluded = ['media_type', 'file_format', 'color_mode', 'color_depth', 'view_settings', 'views_format'] ''' ## all attrs @@ -68,21 +121,61 @@ def set_file_output_format(fo): 'color_mode': 'RGBA' } - for k, v in env_file_format.items(): - setattr(fo.format, k, v) + set_format_settings(fo.format, env_file_format) + +def set_file_output_path(fo, path, suffix=''): + '''Set directory and file name of a file output node from a single path + Multilayer: last element is the file name, ex: "//render/name/name_" + Other formats: whole path is the directory, ex: "//render/name/" + suffix: optional subfolder added at the end of the directory, ex: "tech/" + ''' + if fo.format.file_format == 'OPEN_EXR_MULTILAYER': + directory, file_name = re.match(r'(.*[\\/])?(.*)', path).groups() + directory = directory or '' + else: + directory, file_name = path, '' + + if suffix: + directory = directory.rstrip('\\/') + '/' + suffix if directory else suffix + + fo.directory = directory + fo.file_name = file_name + +## Source socket type to file output item socket type +FILE_OUTPUT_ITEM_TYPES = { + 'VALUE': 'FLOAT', + 'INT': 'INT', + 'BOOLEAN': 'BOOLEAN', + 'VECTOR': 'VECTOR', + 'RGBA': 'RGBA', +} + +def add_file_output_item(fo, name, socket=None): + '''Add an item to file output node + socket: source socket, if passed item type (and vector dimensions) match the socket + return created item and its input socket + ''' + socket_type = FILE_OUTPUT_ITEM_TYPES.get(socket.type, 'RGBA') if socket is not None else 'RGBA' + item = fo.file_output_items.new(socket_type, name) + if socket_type == 'VECTOR' and hasattr(socket, 'default_value'): + ## ex: render layer "Vector" pass is 4D + item.vector_socket_dimensions = len(socket.default_value) + ## items inputs are in the same order (followed by a virtual socket) + return item, fo.inputs[len(fo.file_output_items) - 1] def clear_disconnected(fo): - '''Remove unlinked inputs from file output node''' - for inp in reversed(fo.inputs): - if not inp.is_linked: - print(f'Deleting unlinked fileout slot: {inp.name}') - fo.inputs.remove(inp) + '''Remove unlinked items from file output node''' + items = fo.file_output_items + for i in reversed(range(len(items))): + if not fo.inputs[i].is_linked: + print(f'Deleting unlinked fileout item: {items[i].name}') + items.remove(items[i]) def create_node(type, tree=None, **kargs): '''Get a type, a tree to add in, and optionnaly multiple attribute to set return created node ''' - tree = tree or bpy.context.scene.node_tree + tree = tree or get_compositor_tree() node = tree.nodes.new(type) for k,v in kargs.items(): @@ -129,79 +222,68 @@ def create_and_connect_file_output(node, outputs, file_out, out_name, base_path, """ if not outputs: return None - - nodes = scene.node_tree.nodes - links = scene.node_tree.links - + + tree = get_compositor_tree(scene) + nodes = tree.nodes + links = tree.links + fo = file_out if not fo: fo = nodes.get(out_name) if not fo: - fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, - location=(real_loc(node)[0] + location_offset[0], - real_loc(node)[1] + location_offset[1]), width=600) - fo.inputs.remove(fo.inputs[0]) # Remove default image input - + fo = create_node('CompositorNodeOutputFile', tree=tree, width=600) + fo.file_output_items.clear() # Ensure there is no default item + if file_format: - for k, v in file_format.items(): - setattr(fo.format, k, v) + set_format_settings(fo.format, file_format) else: set_file_output_format(fo) - + if color_depth: fo.format.color_depth = color_depth - + fo.name = out_name if node.parent: fo.parent = node.parent + fo.location_absolute = (real_loc(node)[0] + location_offset[0], + real_loc(node)[1] + location_offset[1]) if base_path_template is not None: - fo.base_path = build_path_from_template(base_path_template, - node_name=node.name.strip(), - node_label=node.label.strip(), - scene_name=node.scene.name if node.type == 'R_LAYERS' else None, - viewlayer_name=node.layer if node.type == 'R_LAYERS' else None, - ) - if suffix: - if fo.format.file_format == 'OPEN_EXR_MULTILAYER': - ## insert suffix right after last slash as folder - path_list = re.split('(\\/)', fo.base_path) - path_list.insert(-1, suffix) - fo.base_path = ''.join(path_list) - else: - ## Ensure slash separator then add suffix - fo.base_path = fo.base_path.rstrip('\\/') + '/' + suffix - else: + path = build_path_from_template(base_path_template, + node_name=node.name.strip(), + node_label=node.label.strip(), + scene_name=node.scene.name if node.type == 'R_LAYERS' else None, + viewlayer_name=node.layer if node.type == 'R_LAYERS' else None, + ) + elif base_path: + path = base_path + elif fo.format.file_format == 'OPEN_EXR_MULTILAYER': # default behavior without template - if base_path: - fo.base_path = base_path - else: - if fo.format.file_format == 'OPEN_EXR_MULTILAYER': - fo.base_path = f'//render/{out_base}/{suffix}{out_base}_' - else: - fo.base_path = f'//render/{out_base}/{suffix}' + path = f'//render/{out_base}/{out_base}_' + else: + path = f'//render/{out_base}/' + + ## suffix is added as a subfolder of the directory + set_file_output_path(fo, path, suffix=suffix) for o in outputs: if next((l for l in o.links if recursive_node_connect_check(l, fo)), None): continue - + if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)): slot_name = bpy.path.clean_name(custom_name) else: slot_name = bpy.path.clean_name(o.name) - fs = fo.file_slots.new('tmp') - ls = fo.layer_slots.new('tmp') + ## Multilayer: item name is the layer name in the file + ## Other formats: item name is the subpath added to directory + file name + if fo.format.file_format == 'OPEN_EXR_MULTILAYER' or use_slot_template: + ## use_slot_template always False (currently never use slot template) + item_name = slot_name + else: + item_name = f'{slot_name}/{slot_name}_' - ls = fo.layer_slots[-1] - ls.name = slot_name - fs = fo.file_slots[-1] - fs.path = f'{slot_name}/{slot_name}_' - if use_slot_template: - ## Always False (currently never use slot template) - fs.path = slot_name - - out_input = fo.inputs[-1] + _item, out_input = add_file_output_item(fo, item_name, socket=o) links.new(o, out_input) clear_disconnected(fo) @@ -451,6 +533,12 @@ def set_properties_editor_tab(tab, skip_if_exists=True): return 1 +def get_object_icon(obj): + '''Return outliner icon name of passed object''' + if obj.type == 'LIGHT_PROBE': + return 'OUTLINER_OB_LIGHTPROBE' + return f'OUTLINER_OB_{obj.type}' + def show_and_active_object(obj, make_active=True, select=True, unhide=True): ''' Show the object @@ -685,7 +773,7 @@ def store_visibility_states(collection=None): def store_collection_states(collection, context=None): - '''Pass a colelction and store visibility state''' + '''Pass a collection and store visibility state''' context = context or bpy.context scene = context.scene diff --git a/operators/conform_collection_hierarchy.py b/operators/conform_collection_hierarchy.py index 916e164..de508ff 100755 --- a/operators/conform_collection_hierarchy.py +++ b/operators/conform_collection_hierarchy.py @@ -244,7 +244,7 @@ class RT_OT_conform_collection_hierarchy(Operator): for k, v in type_dict.items(): if item_type == "objects": - icon = f'OUTLINER_OB_{k.type}' + icon = fn.get_object_icon(k) name = k.name else: icon = 'OUTLINER_COLLECTION' diff --git a/operators/output_management.py b/operators/output_management.py index 9660a10..da55c41 100755 --- a/operators/output_management.py +++ b/operators/output_management.py @@ -11,24 +11,16 @@ from .. import fn # region renumber outputs def delete_numbering(fo): # padding=3 - '''Delete prefix numbering on all slots on passed file output''' + '''Delete prefix numbering on all items on passed file output''' if fo.type != 'OUTPUT_FILE': return - if fo.format.file_format == 'OPEN_EXR_MULTILAYER': - slots = fo.layer_slots - field_attr = 'name' - else: - slots = fo.file_slots - field_attr = 'path' - - for fs in slots: - elems = getattr(fs, field_attr).split('/') + for fs in fo.file_output_items: + elems = fs.name.split('/') for i, e in enumerate(elems): elems[i] = re.sub(r'^\d{3}_', '', e) - new = '/'.join(elems) - setattr(fs, field_attr, new) + fs.name = '/'.join(elems) ## Add - Insert prefix incrementation in fileoutput @@ -43,9 +35,7 @@ def get_numbered_output(out, slot_name): def add_fileslot_number(fs, number): - field_attr = 'name' if hasattr(fs, 'name') else 'path' - - elems = getattr(fs, field_attr).split('/') + elems = fs.name.split('/') for i, e in enumerate(elems): if re.match(r'^\d{3}_', e): elems[i] = re.sub(r'^(\d{3})', lambda x: str(number).zfill(3), e) @@ -53,37 +43,34 @@ def add_fileslot_number(fs, number): elems[i] = f'{str(number).zfill(3)}_{e}' new = '/'.join(elems) - setattr(fs, field_attr, new) + fs.name = new return new def renumber(fo, offset=10): - '''Force renumber all the slots with a 3''' + '''Force renumber all the items with a 3''' if fo.type != 'OUTPUT_FILE': return ct = 10 # start at 10 - slots = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots - for fs in slots: + for fs in fo.file_output_items: add_fileslot_number(fs, ct) ct += offset def get_num(string) -> int: - '''get a tring or a file_slot object + '''get a string or a file output item object return leading number or None ''' if not isinstance(string, str): - if hasattr(string, 'path'): - string = string.path - else: - string = string.name + string = string.name num = re.search(r'^(\d{3})_', string) if num: return int(num.group(1)) def reverse_fileout_inputs(fo): - count = len(fo.inputs) + items = fo.file_output_items + count = len(items) for i in range(count): - fo.inputs.move(count-1, i) + items.move(count-1, i) def renumber_keep_existing(fo, offset=10, invert=True): '''Renumber by keeping existing numbers and inserting new one whenever possible @@ -96,7 +83,7 @@ def renumber_keep_existing(fo, offset=10, invert=True): if invert: reverse_fileout_inputs(fo) - fsl = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots + fsl = fo.file_output_items last_idx = len(fsl) - 1 prev = None @@ -140,7 +127,7 @@ def renumber_keep_existing(fo, offset=10, invert=True): if next_num is None: add_fileslot_number(fs, 0) elif next_num == 0: - print(f'Cannot insert value before 0 to {fsl.path}') + print(f'Cannot insert value before 0 to {fs.name}') continue else: add_fileslot_number(fs, int(next_num / 2)) @@ -161,7 +148,7 @@ def renumber_keep_existing(fo, offset=10, invert=True): # iterate rename gap_inc = int((next_num - prev_num) / divider) if gap_inc < 1: # same values ! - print(f'cannot insert a median value at {fs.path} between {prev_num} and {next_num}') + print(f'cannot insert a median value at {fs.name} between {prev_num} and {next_num}') continue ct = prev_num @@ -187,7 +174,7 @@ class RT_OT_number_outputs(Operator): @classmethod def poll(cls, context): - return True + return fn.get_compositor_tree(context.scene) is not None mode : StringProperty(default='SELECTED', options={'SKIP_SAVE'}) clear : BoolProperty(default=False, options={'SKIP_SAVE'}) @@ -199,10 +186,8 @@ class RT_OT_number_outputs(Operator): return self.execute(context) def execute(self, context): - scn = context.scene - ct = 0 - nodes = scn.node_tree.nodes + nodes = fn.get_compositor_tree(context.scene).nodes for fo in nodes: if fo.type != 'OUTPUT_FILE': continue @@ -234,9 +219,13 @@ class RT_OT_mute_toggle_output_nodes(Operator): mute : BoolProperty(default=True, options={'SKIP_SAVE'}) + @classmethod + def poll(cls, context): + return fn.get_compositor_tree(context.scene) is not None + def execute(self, context): ct = 0 - for n in context.scene.node_tree.nodes: + for n in fn.get_compositor_tree(context.scene).nodes: if n.type != 'OUTPUT_FILE': continue n.mute = self.mute @@ -255,8 +244,12 @@ class RT_OT_set_output_node_format(Operator): mute : BoolProperty(default=True, options={'SKIP_SAVE'}) + @classmethod + def poll(cls, context): + return fn.get_compositor_tree(context.scene) is not None + def execute(self, context): - nodes = context.scene.node_tree.nodes + nodes = fn.get_compositor_tree(context.scene).nodes if not nodes.active or nodes.active.type != 'OUTPUT_FILE': self.report({"ERROR"}, f'Active node should be an output file to use as reference for output format') return {"CANCELLED"} @@ -273,8 +266,14 @@ class RT_OT_set_output_node_format(Operator): if n.type != 'OUTPUT_FILE' or n == ref or not n.select: continue + ## media type and file format first, available values of other attributes depend on them + n.format.media_type = ref.format.media_type + n.format.file_format = ref.format.file_format + for attr in dir(ref.format): - if attr.startswith('__') or attr in {'rna_type','bl_rna', 'view_settings', 'display_settings','stereo_3d_format'}: # views_format + if attr.startswith('__') or attr in {'rna_type','bl_rna', 'media_type', 'file_format', + 'view_settings', 'display_settings', 'linear_colorspace_settings', + 'has_linear_colorspace', 'stereo_3d_format'}: # views_format continue try: setattr(n.format, attr, getattr(ref.format, attr)) @@ -295,36 +294,38 @@ class RT_OT_set_output_node_format(Operator): class RT_OT_set_active_file_output_slot_to_composite(bpy.types.Operator): bl_idname = "rt.set_active_file_output_slot_to_composite" - bl_label = "Set Active File Output Slot To Composite" - bl_description = "Use active slot of active file output node to set scene output settings (swap connection)" + bl_label = "Set Active File Output Item To Composite" + bl_description = "Use active item of active file output node to set scene output settings\ + \nand swap connection to compositor Group Output node" bl_options = {"REGISTER"} @classmethod def poll(cls, context): - return context.scene.use_nodes\ - and context.scene.node_tree\ - and context.scene.node_tree.nodes.active\ - and context.scene.node_tree.nodes.active.type == 'OUTPUT_FILE' + tree = fn.get_compositor_tree(context.scene) + return bool(tree\ + and tree.nodes.active\ + and tree.nodes.active.type == 'OUTPUT_FILE') relink_composite : bpy.props.BoolProperty( name='Relink Composite', default=True, - description='In case file slot is linked, swap link to Composite file', + description='In case file output item is linked, swap link to compositor Group Output', options={'SKIP_SAVE'}, ) def invoke(self, context, event): - self.fo = context.scene.node_tree.nodes.active - if not len(self.fo.file_slots): - self.report({'ERROR'}, 'no slots in active file output') + tree = fn.get_compositor_tree(context.scene) + self.fo = tree.nodes.active + if not len(self.fo.file_output_items): + self.report({'ERROR'}, 'no items in active file output') return {'CANCELLED'} - # check if active slot has a source - if not self.fo.inputs[self.fo.active_input_index].is_linked: + # check if active item has a source + if not self.fo.inputs[self.fo.active_item_index].is_linked: return self.execute(context) # check if composite linked - out = context.scene.node_tree.nodes.get('Composite') + out = fn.get_group_output_node(tree) if not out or not out.inputs[0].is_linked: self.compo_out_from_link = '' return self.execute(context) @@ -337,34 +338,36 @@ class RT_OT_set_active_file_output_slot_to_composite(bpy.types.Operator): def draw(self, context): layout = self.layout col = layout.column() - col.label(text=f'Composite node connected to: {self.compo_out_from_link}') - col.label(text=f'Would you like to replace by file output slot source ?') + col.label(text=f'Group Output node connected to: {self.compo_out_from_link}') + col.label(text=f'Would you like to replace by file output item source ?') layout.prop(self, 'relink_composite') def execute(self, context): + self.fo = fn.get_compositor_tree(context.scene).nodes.active + if not len(self.fo.file_output_items): + self.report({'ERROR'}, 'no items in active file output') + return {'CANCELLED'} + # if comp fn.set_scene_output_from_active_fileout_item() - idx = self.fo.active_input_index - sl = self.fo.file_slots[idx] + idx = self.fo.active_item_index + item = self.fo.file_output_items[idx] sk = self.fo.inputs[idx] if not sk.is_linked: - self.report({'INFO'}, f'Outut changed to match {sl.path} (slot was not linked)') + self.report({'INFO'}, f'Outut changed to match {item.name} (item was not linked)') return {'FINISHED'} - ## If linked replace links to Composite node + ## If linked replace links to Group Output node if not self.relink_composite: return {'FINISHED'} - ntree = context.scene.node_tree - links = context.scene.node_tree.links - nodes = context.scene.node_tree.nodes + ntree = fn.get_compositor_tree(context.scene) + links = ntree.links - out = nodes.get('Composite') - if not out: - out = fn.create_node('COMPOSITE', tree=ntree) + out = fn.get_group_output_node(ntree, create=True) fo_loc = fn.real_loc(self.fo) - out.location = (fo_loc.x, fo_loc.y + 160) + out.location_absolute = (fo_loc.x, fo_loc.y + 160) # if out.inputs[0].is_linked: # self.report({'WARNING'}, f'Outut changed to match {sl.path} (Composite node already linked)') diff --git a/operators/output_search_and_replace.py b/operators/output_search_and_replace.py index 708f25e..e16d420 100755 --- a/operators/output_search_and_replace.py +++ b/operators/output_search_and_replace.py @@ -10,7 +10,7 @@ from bpy.props import (StringProperty, 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_description = "Search/Replace texts in output paths (directory and file name) and items" bl_options = {'REGISTER', 'UNDO'} ## Search and replace options @@ -22,9 +22,9 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator): 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"), + ('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' ) @@ -46,7 +46,8 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator): @classmethod def poll(cls, context): - return context.scene.node_tree and context.scene.node_tree.nodes + tree = fn.get_compositor_tree(context.scene) + return bool(tree and tree.nodes) def rename(self, source): if not self.find: @@ -133,12 +134,12 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator): # return ## Checked in poll - node_tree = context.scene.node_tree - + 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'} @@ -148,29 +149,21 @@ class RT_OT_outputs_search_and_replace(bpy.types.Operator): 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 + 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 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 + 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: diff --git a/operators/output_setup.py b/operators/output_setup.py index 2db8fd3..8072196 100755 --- a/operators/output_setup.py +++ b/operators/output_setup.py @@ -324,9 +324,16 @@ class RT_OT_create_output_layers(bpy.types.Operator): self.socket_collection = context.window_manager.rt_socket_collection self.socket_collection.clear() + + tree = fn.get_compositor_tree(context.scene) + if tree is None: + self.report({'ERROR'}, 'Scene has no compositing node group') + return {'CANCELLED'} + if event.ctrl: # Direct connect, do not use any options self.base_path = '' + self.final_base_path_template = None return self.execute(context) ## Store variable usable in templates @@ -344,7 +351,7 @@ class RT_OT_create_output_layers(bpy.types.Operator): tech_passes_names = [tp.strip().lower() for tp in tech_passes_names.split(',') if tp.strip()] - selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE'] + selected = [n for n in tree.nodes if n.select and n.type != 'OUTPUT_FILE'] if not selected: self.report({'ERROR'}, 'No render layer nodes selected') return {'CANCELLED'} @@ -411,9 +418,21 @@ class RT_OT_create_output_layers(bpy.types.Operator): if ref.type == 'R_LAYERS': self.scene_name = ref.scene.name self.view_layer_name = ref.layer - self.final_base_path_template = '' + ## Set here too, since draw only update it when settings are expanded + self.final_base_path_template = self.get_base_path_template(context) return context.window_manager.invoke_props_dialog(self, width=530) + def get_base_path_template(self, context): + '''Return base path template from scene settings, None if templates are disabled''' + if not self.use_base_path_templates: + return None + final_format = self.file_format + if final_format == 'NONE': + final_format = os.environ.get('FILE_FORMAT', 'OPEN_EXR_MULTILAYER') + if final_format == 'OPEN_EXR_MULTILAYER': + return context.scene.render_toolbox.default_multilayer_base_path + return context.scene.render_toolbox.default_base_path + def draw(self, context): layout = self.layout box = layout.box() @@ -530,7 +549,7 @@ Possible variables: 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) + rln = fn.get_compositor_tree(context.scene).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') @@ -623,8 +642,7 @@ Possible variables: 'color_depth' : '32', } - scn = context.scene - nodes = scn.node_tree.nodes + nodes = fn.get_compositor_tree(context.scene).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 diff --git a/operators/simplify_conflicts.py b/operators/simplify_conflicts.py index 8665712..cca4a99 100755 --- a/operators/simplify_conflicts.py +++ b/operators/simplify_conflicts.py @@ -7,6 +7,8 @@ from bpy.props import (BoolProperty, CollectionProperty, StringProperty) +from .. import fn + def list_simplify_affected_objects(context): """Return a list of object affected by simplify return : [[obj, [{type: "SUBSURF", ..}, {..}], icon], ..] @@ -103,7 +105,7 @@ def list_simplify_affected_objects(context): # If object has affected data, add it to the list if affected_data: - ob_list.append([obj, affected_data, "OUTLINER_OB_" + obj.type]) + ob_list.append([obj, affected_data, fn.get_object_icon(obj)]) return ob_list @@ -288,7 +290,7 @@ class RT_OT_list_object_affected_by_simplify(Operator): # Create subrow for hair info subrow = row.row() - subrow.label(text=f"Hair: {mod.name}", icon='HAIR') + subrow.label(text=f"Hair: {mod.name}", icon='CURVES_DATA') subrow.label(text=f"Node Group: {data['node_group']}") elif data['type'] == 'SUBSURFACE': diff --git a/operators/viewlayer_management.py b/operators/viewlayer_management.py index 8a784ae..0918ebf 100755 --- a/operators/viewlayer_management.py +++ b/operators/viewlayer_management.py @@ -5,6 +5,8 @@ from bpy.props import (StringProperty, BoolProperty, ) +from .. import fn + class RT_OT_enable_all_viewlayers(Operator): bl_idname = "rt.enable_all_viewlayers" bl_label = "Enable All Viewlayers" @@ -27,10 +29,14 @@ class RT_OT_activate_only_selected_layers(Operator): bl_description = "Activate only selected node view layer , excluding all others" bl_options = {"REGISTER"} + @classmethod + def poll(cls, context): + return fn.get_compositor_tree(context.scene) is not None + def execute(self, context): scn = context.scene - nodes = scn.node_tree.nodes + nodes = fn.get_compositor_tree(scn).nodes rlayers_nodes = [n for n in nodes if n.select and n.type == 'R_LAYERS'] vls = [scn.view_layers.get(n.layer) for n in rlayers_nodes if scn.view_layers.get(n.layer)] diff --git a/operators/visibility_conflicts.py b/operators/visibility_conflicts.py index ede174d..95ee0ef 100755 --- a/operators/visibility_conflicts.py +++ b/operators/visibility_conflicts.py @@ -425,7 +425,7 @@ class RT_OT_list_modifier_visibility(Operator): for m in o.modifiers: if m.show_viewport != m.show_render: if not mods: - self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type]) + self.ob_list.append([o, mods, fn.get_object_icon(o)]) mods.append(m) self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name) return context.window_manager.invoke_props_dialog(self, width=350) diff --git a/preferences.py b/preferences.py index 13847dd..748aab0 100755 --- a/preferences.py +++ b/preferences.py @@ -133,7 +133,7 @@ def register(): bpy.app.handlers.load_post.append(set_render_toolbox_settings) def unregister(): - if not 'set_render_toolbox_settings' in [hand.__name__ for hand in bpy.app.handlers.load_post]: + if 'set_render_toolbox_settings' in [hand.__name__ for hand in bpy.app.handlers.load_post]: bpy.app.handlers.load_post.remove(set_render_toolbox_settings) for cls in reversed(classes): diff --git a/ui.py b/ui.py index 1475138..b1a4ec4 100755 --- a/ui.py +++ b/ui.py @@ -68,14 +68,15 @@ class RT_PT_render_toolbox_ui(Panel): col.prop(scn.render, 'resolution_percentage', text="Resolution Percentage") percent = scn.render.resolution_percentage col.label(text=f"{int(scn.render.resolution_x * percent / 100)}x{int(scn.render.resolution_y * percent / 100)}", icon='INFO') - if cam and cam.data.shift_x != 0 or cam.data.shift_y != 0: + if cam and (cam.data.shift_x != 0 or cam.data.shift_y != 0): col.label(text='Camera has Shift', icon='INFO') # col.prop(cam.data, 'shift_x', text="Shift X") # col.prop(cam.data, 'shift_y', text="Shift Y") - + ## viewlayer section layout.label(text='View layers:') - ct = len([n for n in context.scene.node_tree.nodes if n.type == 'R_LAYERS' and n.select]) + tree = fn.get_compositor_tree(scn) + ct = len([n for n in tree.nodes if n.type == 'R_LAYERS' and n.select]) if tree else 0 # col = layout.column(align=True) # row=col.row(align=True) @@ -109,13 +110,18 @@ class RT_PT_file_output_ui(bpy.types.Panel): def draw(self, context): layout = self.layout + tree = fn.get_compositor_tree(context.scene) + if tree is None: + layout.label(text='Scene has no compositing node group', icon='INFO') + return + col = layout.column() col.operator("rt.create_output_layers", text='Create File Output', icon="NODE") col.operator("rt.outputs_search_and_replace", text='Search And Replace Outputs', icon="BORDERMOVE") col.separator() col.operator('rt.set_output_node_format', icon='OUTPUT', text='Copy Output Format To Selected') - col.operator('rt.set_active_file_output_slot_to_composite', icon='OUTPUT', text='Active Slot To Composite') + col.operator('rt.set_active_file_output_slot_to_composite', icon='OUTPUT', text='Active Item To Composite') layout.label(text='All Outputs:') @@ -123,17 +129,15 @@ class RT_PT_file_output_ui(bpy.types.Panel): row.operator('rt.mute_toggle_output_nodes', icon='NODE_INSERT_ON', text='Mute').mute = True row.operator('rt.mute_toggle_output_nodes', icon='NODE_INSERT_OFF', text='Unmute').mute = False - scn = context.scene - - disabled_output = [n for n in scn.node_tree.nodes if n.type == 'OUTPUT_FILE' and n.mute] + disabled_output = [n for n in tree.nodes if n.type == 'OUTPUT_FILE' and n.mute] if disabled_output: - output_ct = len([n for n in scn.node_tree.nodes if n.type == 'OUTPUT_FILE']) + output_ct = len([n for n in tree.nodes if n.type == 'OUTPUT_FILE']) layout.label(text=f'{len(disabled_output)}/{output_ct} Output Muted', icon='INFO') col = layout.column() ## (re)number exports - ct = len([n for n in context.scene.node_tree.nodes if n.type == 'OUTPUT_FILE' and n.select]) + ct = len([n for n in tree.nodes if n.type == 'OUTPUT_FILE' and n.select]) txt = f'Renumber {ct} Selected Outputs' subcol = col.column() subcol.enabled = bool(ct)