From b6b090d4ea8d252ca915d08dcbbd371869b217e5 Mon Sep 17 00:00:00 2001 From: pullusb Date: Wed, 30 Jul 2025 18:16:32 +0200 Subject: [PATCH] Migrate Fileoutput and viewlayer management ops from gp_render Better overall UI. Split file output in a separate panel --- fn.py | 38 +++++++ operators/__init__.py | 2 + operators/output_management.py | 111 ++++++++++++++------ operators/viewlayer_management.py | 54 ++++++++++ ui.py | 169 +++++++++++++++++++++++++++--- 5 files changed, 327 insertions(+), 47 deletions(-) create mode 100644 operators/viewlayer_management.py diff --git a/fn.py b/fn.py index 45ce502..0365433 100755 --- a/fn.py +++ b/fn.py @@ -6,6 +6,7 @@ import json from .constant import TECH_PASS_KEYWORDS from pathlib import Path + # region Manage nodes def real_loc(n): @@ -13,6 +14,43 @@ def real_loc(n): return n.location return n.location + real_loc(n.parent) +def set_scene_output_from_active_fileout_item(): + scn = bpy.context.scene + rd = scn.render + ntree = scn.node_tree + 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) + + 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.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) + + excluded = ['file_format', 'color_mode', 'color_depth', + 'view_settings', 'views_format'] + + ''' ## all attrs + # 'cineon_black', 'cineon_gamma', 'cineon_white', + # 'color_depth', 'color_mode', 'compression', 'display_settings', + # 'exr_codec', 'file_format', 'jpeg2k_codec', 'quality', + # 'rna_type', 'stereo_3d_format', 'tiff_codec', 'use_cineon_log', + # 'use_jpeg2k_cinema_48', 'use_jpeg2k_cinema_preset', 'use_jpeg2k_ycc', + # 'use_preview', 'use_zbuffer'] + ''' + + for attr in dir(fmt): + if attr.startswith('__') or attr.startswith('bl_') or attr in excluded: + continue + if hasattr(scn.render.image_settings, attr) and not scn.render.image_settings.is_property_readonly(attr): + setattr(scn.render.image_settings, attr, getattr(fmt, attr)) + def set_file_output_format(fo): """Default fileout format for output file node Get from OUTPUT_RENDER_FILE_FORMAT environment variable diff --git a/operators/__init__.py b/operators/__init__.py index 4b2a26a..ffd3957 100755 --- a/operators/__init__.py +++ b/operators/__init__.py @@ -3,6 +3,7 @@ from . import ( output_search_and_replace, output_setup, output_management, + viewlayer_management, visibility_conflicts, simplify_conflicts, store_visibility_states, @@ -15,6 +16,7 @@ mods = ( output_setup, output_search_and_replace, output_management, + viewlayer_management, visibility_conflicts, simplify_conflicts, store_visibility_states, diff --git a/operators/output_management.py b/operators/output_management.py index bdf892f..9660a10 100644 --- a/operators/output_management.py +++ b/operators/output_management.py @@ -6,6 +6,7 @@ from bpy.props import (StringProperty, BoolProperty, ) +from .. import fn # region renumber outputs @@ -177,7 +178,7 @@ def renumber_keep_existing(fo, offset=10, invert=True): if invert: reverse_fileout_inputs(fo) -class RT_OT_number_outputs(bpy.types.Operator): +class RT_OT_number_outputs(Operator): bl_idname = "rt.number_outputs" bl_label = "Number Outputs" bl_description = "(Re)Number the outputs to have ordered file by name in export directories\ @@ -225,7 +226,7 @@ class RT_OT_number_outputs(bpy.types.Operator): # region file output -class RT_OT_mute_toggle_output_nodes(bpy.types.Operator): +class RT_OT_mute_toggle_output_nodes(Operator): bl_idname = "rt.mute_toggle_output_nodes" bl_label = "Mute Toggle output nodes" bl_description = "Mute / Unmute all output nodes" @@ -246,10 +247,10 @@ class RT_OT_mute_toggle_output_nodes(bpy.types.Operator): return {"FINISHED"} -class RT_OT_set_output_node_format(bpy.types.Operator): +class RT_OT_set_output_node_format(Operator): bl_idname = "rt.set_output_node_format" - bl_label = "Set output format from active" - bl_description = "Change all selected output node to match active output node format" + bl_label = "Set file output node format from active" + bl_description = "Change all selected file output node format to match active" bl_options = {"REGISTER"} mute : BoolProperty(default=True, options={'SKIP_SAVE'}) @@ -292,50 +293,94 @@ class RT_OT_set_output_node_format(bpy.types.Operator): return {"FINISHED"} -# region view layers - -class RT_OT_enable_all_viewlayers(bpy.types.Operator): - bl_idname = "rt.enable_all_viewlayers" - bl_label = "Enable All Viewlayers" - bl_description = "Enable all View layers except those named 'exclude' 'View Layer'" +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_options = {"REGISTER"} - def execute(self, context): - scn = context.scene + @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' - vl_list = [vl for vl in scn.view_layers if not vl.use and vl.name not in {'View Layer', 'exclude'}] - for v in vl_list: - v.use = True + relink_composite : bpy.props.BoolProperty( + name='Relink Composite', + default=True, + description='In case file slot is linked, swap link to Composite file', + options={'SKIP_SAVE'}, + ) - self.report({"INFO"}, f'{len(vl_list)} ViewLayers Reactivated') - return {"FINISHED"} + 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') + return {'CANCELLED'} -class RT_OT_activate_only_selected_layers(bpy.types.Operator): - bl_idname = "rt.activate_only_selected_layers" - bl_label = "Activate Only Selected Layers" - bl_description = "Activate only selected node view layer , excluding all others" - bl_options = {"REGISTER"} + # check if active slot has a source + if not self.fo.inputs[self.fo.active_input_index].is_linked: + return self.execute(context) + + # check if composite linked + out = context.scene.node_tree.nodes.get('Composite') + if not out or not out.inputs[0].is_linked: + self.compo_out_from_link = '' + return self.execute(context) + + # compo linked, pop panel to choose replace or not + self.compo_out_from_link = out.inputs[0].links[0].from_node.name + return context.window_manager.invoke_props_dialog(self) + + + 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 ?') + layout.prop(self, 'relink_composite') def execute(self, context): - scn = context.scene + # if comp + fn.set_scene_output_from_active_fileout_item() + idx = self.fo.active_input_index + sl = self.fo.file_slots[idx] + sk = self.fo.inputs[idx] - nodes = scn.node_tree.nodes + if not sk.is_linked: + self.report({'INFO'}, f'Outut changed to match {sl.path} (slot was not linked)') + return {'FINISHED'} - 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)] - for v in scn.view_layers: - v.use = v in vls + ## If linked replace links to Composite 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 + + out = nodes.get('Composite') + if not out: + out = fn.create_node('COMPOSITE', tree=ntree) + fo_loc = fn.real_loc(self.fo) + out.location = (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)') + + lnk = sk.links[0] + from_sk = sk.links[0].from_socket + links.remove(lnk) + links.new(from_sk, out.inputs[0]) - self.report({"INFO"}, f'Now only {len(vls)} viewlayer active (/{len(scn.view_layers)})') return {"FINISHED"} - classes = ( RT_OT_number_outputs, RT_OT_mute_toggle_output_nodes, RT_OT_set_output_node_format, - RT_OT_enable_all_viewlayers, - RT_OT_activate_only_selected_layers + RT_OT_set_active_file_output_slot_to_composite, ) def register(): diff --git a/operators/viewlayer_management.py b/operators/viewlayer_management.py new file mode 100644 index 0000000..8a784ae --- /dev/null +++ b/operators/viewlayer_management.py @@ -0,0 +1,54 @@ +import bpy + +from bpy.types import Operator +from bpy.props import (StringProperty, + BoolProperty, + ) + +class RT_OT_enable_all_viewlayers(Operator): + bl_idname = "rt.enable_all_viewlayers" + bl_label = "Enable All Viewlayers" + bl_description = "Enable all View layers except those named 'exclude'" # 'View Layer' + bl_options = {"REGISTER"} + + def execute(self, context): + scn = context.scene + + vl_list = [vl for vl in scn.view_layers if not vl.use and vl.name not in {'exclude',}] # 'View Layer', + for v in vl_list: + v.use = True + + self.report({"INFO"}, f'{len(vl_list)} ViewLayers Reactivated') + return {"FINISHED"} + +class RT_OT_activate_only_selected_layers(Operator): + bl_idname = "rt.activate_only_selected_layers" + bl_label = "Activate Only Selected Layers" + bl_description = "Activate only selected node view layer , excluding all others" + bl_options = {"REGISTER"} + + def execute(self, context): + scn = context.scene + + nodes = scn.node_tree.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)] + for v in scn.view_layers: + v.use = v in vls + + self.report({"INFO"}, f'Now only {len(vls)} viewlayer active (/{len(scn.view_layers)})') + return {"FINISHED"} + +classes = ( + RT_OT_enable_all_viewlayers, + RT_OT_activate_only_selected_layers +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) diff --git a/ui.py b/ui.py index 3c7d13b..1475138 100755 --- a/ui.py +++ b/ui.py @@ -4,6 +4,41 @@ from bpy.types import Panel from . import fn +# region viewlayer layout + +def viewlayer_layout(layout, scn): + for vl in scn.view_layers: + row = layout.row() + row.prop(vl, 'use', text=vl.name, icon='RESTRICT_RENDER_OFF' if vl.use else 'RESTRICT_RENDER_ON', emboss=False, toggle=0) + +class RT_PT_viewlayers_ui(Panel): + bl_space_type = "NODE_EDITOR" + bl_region_type = "UI" + bl_label = "View Layers" + + def draw(self, context): + layout = self.layout + layout.label(text=f'{context.scene.name} :: View layers') + col = layout.column(align=True) + viewlayer_layout(col, context.scene) + +class RT_PT_viewlayers_multi_ui(Panel): + bl_space_type = "NODE_EDITOR" + bl_region_type = "UI" + bl_label = "Multi View Layers" + + def draw(self, context): + layout = self.layout + layout.label(text=f'{len(bpy.data.scenes)} scenes view layers:') + for s in bpy.data.scenes: + col = layout.column() + # col.label(text=f'{s.name}:') + col.label(text=s.name) + viewlayer_layout(col, s) + layout.separator() + +# region main panel + class RT_PT_render_toolbox_ui(Panel): bl_space_type = "NODE_EDITOR" bl_region_type = "UI" @@ -12,10 +47,106 @@ class RT_PT_render_toolbox_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") - # layout.separator() + ## Scene infos recap + scn = context.scene + text = f'{scn.render.resolution_x}x{scn.render.resolution_y} @ {scn.render.fps} fps' + cam = context.scene.camera + if cam: + + text = f'{cam.name} : {text}' + else: + text = f'No Camera ! : {text}' + + box = layout.box() + col = box.column() + col.label(text=text, icon='SCENE_DATA') # VIEW_CAMERA + # col.label(text=f"{scn.render.fps} fps") + + if scn.render.resolution_percentage != 100: + col.label(text='Percentage not 100%', icon='INFO') + 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: + 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]) + + # col = layout.column(align=True) + # row=col.row(align=True) + col = layout.column(align=False) + row = col.row(align=True) + row.operator("wm.call_panel", text="View layers", icon='RENDERLAYERS').name = "RT_PT_viewlayers_ui" + row.operator("wm.call_panel", text="All View layers", icon='SCENE_DATA').name = "RT_PT_viewlayers_multi_ui" + + # row=layout.row(align=True) + + row1 = col.row(align=True) + row1.operator('rt.activate_only_selected_layers', text=f'Activate Only {ct} RenderLayer Nodes') + row1.enabled = ct > 0 + + exclude_count = len([vl for vl in scn.view_layers if not vl.use and vl.name not in {'exclude',}]) # 'View Layer', + if exclude_count: + # layout.label(text=f'{exclude_count} Excluded View Layers !') + layout.operator('rt.enable_all_viewlayers', text=f'Reactivate {exclude_count} Excluded View Layers') + + # col = layout.column() + # col.label(text='Clean and updates:') + # col.operator('rt.clean_compo_tree', icon='BRUSHES_ALL', text='Clean Nodes') # NODE_CORNER + +# region file output ui +class RT_PT_file_output_ui(bpy.types.Panel): + bl_space_type = "NODE_EDITOR" + bl_region_type = "UI" + bl_category = "Render" + bl_label = "File Output Manager" + # bl_parent_id = "RT_PT_render_toolbox_ui" + + def draw(self, context): + layout = self.layout + 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') + + layout.label(text='All Outputs:') + + row=layout.row(align=True) + 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] + if disabled_output: + output_ct = len([n for n in scn.node_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]) + txt = f'Renumber {ct} Selected Outputs' + subcol = col.column() + subcol.enabled = bool(ct) + + row = subcol.row(align=True) + row.operator('rt.number_outputs', icon='LINENUMBERS_ON', text=txt).mode = 'SELECTED' + op = row.operator('rt.number_outputs', icon='X', text='') + op.mode = 'SELECTED' + op.clear = True + + + +# region visibility ui # Base panel for drawing class RT_PT_visibility_check_ui_base(bpy.types.Panel): @@ -31,13 +162,14 @@ class RT_PT_visibility_check_ui_base(bpy.types.Panel): def draw(self, context): layout = self.layout - # layout.label(text="Visibily Checks:") - layout.operator("rt.list_object_visibility_conflicts", icon="OBJECT_DATAMODE") - layout.operator("rt.list_viewport_render_visibility", text="List Viewport Vs Render Visibility", icon="OBJECT_DATAMODE") - - layout.operator("rt.list_modifier_visibility", text="List Modifiers Visibility Conflicts", icon="MODIFIER") - - layout.operator("rt.list_collection_visibility_conflicts", text="List Collections Visibility Conflicts", icon="OUTLINER_COLLECTION") + + col = layout.column(align=True) + col .label(text="List Visibility Conflicts:") # , icon='HIDE_OFF' + row = col.row(align=True) + row.operator("rt.list_object_visibility_conflicts", text="Objects", icon="OBJECT_DATAMODE") + row.operator("rt.list_viewport_render_visibility", text="Viewport Vs Render") # , icon="OBJECT_DATAMODE" + col.operator("rt.list_modifier_visibility", text="Modifiers", icon="MODIFIER") + col.operator("rt.list_collection_visibility_conflicts", text="Collections", icon="OUTLINER_COLLECTION") layout.separator() layout.operator("rt.list_object_affected_by_simplify", text="List Object Affected By Simplify", icon="MOD_SIMPLIFY") @@ -53,6 +185,8 @@ class RT_PT_visibility_check_ui_node(RT_PT_visibility_check_ui_base): bl_region_type = 'UI' bl_category = "Render" # Wrangler ? +# region conformation ui + class RT_PT_conformation_ui(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" @@ -100,7 +234,9 @@ class RT_PT_conformation_ui(bpy.types.Panel): # tgt_row = layout.row(align=True) # tgt_row.label(text="", icon='TRIA_RIGHT') tgt_row.label(text=ref_collection.name, icon='OUTLINER_COLLECTION') - col.operator("rt.store_visibility_states", text='Store target State', icon="DISK_DRIVE") + + col = layout.column(align=False) + col.operator("rt.store_visibility_states", text='Store Target Hierarchy State', icon="DISK_DRIVE") ## Show current collection state (behave badly when changed, should be tweaked before) # col = layout.column(align=True) @@ -118,7 +254,7 @@ class RT_PT_conformation_ui(bpy.types.Panel): col = layout.column(align=True) row = col.row(align=True) - row.label(text="Parameter To Conform:") + row.label(text="To Conform:") ## Same order, greyout unused options collec_row = row.row(align=True) collec_row.prop(props, "conform_exclude", text="", icon='CHECKBOX_DEHLT' if ref_vlc.exclude else 'CHECKBOX_HLT') # Exclude from View Layer @@ -148,7 +284,7 @@ class RT_PT_conformation_ui(bpy.types.Panel): tgt_row.label(text=ref_obj.name, icon='OBJECT_DATA') if not ref_obj.children_recursive: - tgt_row.label(text="Object has no children", icon='ERROR') + tgt_row.label(text="No Children", icon='ERROR') return layout.separator() @@ -165,7 +301,7 @@ class RT_PT_conformation_ui(bpy.types.Panel): layout.separator() col = layout.column(align=True) row = col.row(align=True) - row.label(text="Parameter To Conform:") + row.label(text="To Conform:") row.prop(props, "conform_selectability", text="", icon='RESTRICT_SELECT_ON' if ref_obj.hide_select else 'RESTRICT_SELECT_OFF') # Hide Select row.prop(props, "conform_viewlayer", text="", icon='HIDE_ON' if ref_obj.hide_get() else 'HIDE_OFF') # Hide in current viewlayer (eye) row.prop(props, "conform_viewport", text="", icon='RESTRICT_VIEW_ON' if ref_obj.hide_viewport else 'RESTRICT_VIEW_OFF') # Disable in Viewports @@ -175,6 +311,8 @@ class RT_PT_conformation_ui(bpy.types.Panel): layout.operator("rt.conform_collection_hierarchy",text="Conform Hierarchy", icon="CHECKMARK") +# region outliner state + class RT_PT_outliner_state_ui(bpy.types.Panel): bl_space_type = "VIEW_3D" bl_region_type = "UI" @@ -235,7 +373,10 @@ class RT_PT_outliner_state_ui(bpy.types.Panel): classes = ( + RT_PT_viewlayers_ui, + RT_PT_viewlayers_multi_ui, RT_PT_render_toolbox_ui, + RT_PT_file_output_ui, RT_PT_visibility_check_ui_viewport, RT_PT_visibility_check_ui_node, RT_PT_conformation_ui,