diff --git a/fn.py b/fn.py index 9d07cfc..60a0877 100755 --- a/fn.py +++ b/fn.py @@ -305,7 +305,7 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None # endregion -# region Utilities +# region Collections def get_collection_children_recursive(col, cols=None) -> list: '''return a list of all the child collections @@ -319,6 +319,51 @@ def get_collection_children_recursive(col, cols=None) -> list: cols = get_collection_children_recursive(sub, cols) return cols +def get_view_layer_collection(col, vl_col=None, view_layer=None): + '''return viewlayer collection from collection + col: the collection to get viewlayer collection from + view_layer (viewlayer, optional) : viewlayer to search in, if not passed, use active viewlayer + + ''' + if vl_col is None: + if view_layer: + vl_col = view_layer.layer_collection + else: + vl_col = bpy.context.view_layer.layer_collection + for sub in vl_col.children: + if sub.collection == col: + return sub + if len(sub.children): + c = get_view_layer_collection(col, sub) + if c is not None: + return c + +def get_parents_cols(col, root=None, scene=None, cols=None): + '''Return a list of parents collections of passed col + root : Pass a collection to search in (recursive) + Else search in master collection + scene: scene to search in (active scene if not passed) + cols: used internally by the function to collect results + ''' + if cols is None: + cols = [] + + if root == None: + scn = scene or bpy.context.scene + root=scn.collection + + for sub in root.children: + if sub == col: + cols.append(root) + + if len(sub.children): + cols = get_parents_cols(col, root=sub, cols=cols) + return cols + +# endregion + +# region utilities + def set_properties_editor_tab(tab, skip_if_exists=True): '''Take a tab name and apply it to properties editor tab: identifier of the tab, possible name in: diff --git a/operators/__init__.py b/operators/__init__.py index 52be160..c8558a3 100755 --- a/operators/__init__.py +++ b/operators/__init__.py @@ -4,7 +4,7 @@ from . import ( outputs_search_and_replace, visibility_conflicts, simplify_conflicts, - # conform_collection_hierarchy, + conform_collection_hierarchy, scene_checker, ) @@ -14,7 +14,7 @@ mods = ( outputs_search_and_replace, visibility_conflicts, simplify_conflicts, - # conform_collection_hierarchy, + conform_collection_hierarchy, scene_checker, ) diff --git a/operators/conform_collection_hierarchy.py b/operators/conform_collection_hierarchy.py new file mode 100644 index 0000000..55885ba --- /dev/null +++ b/operators/conform_collection_hierarchy.py @@ -0,0 +1,180 @@ +import bpy +from bpy.types import Operator +from bpy.props import (BoolProperty, + EnumProperty, + PointerProperty, + CollectionProperty, + StringProperty) +from .. import fn + +def collection_search_callback(self, context, edit_text): + """Search callback for collection names""" + ## second arg is not displayed, can be and empty string... + return [(c.name, str(c.session_uid)) for c in bpy.context.scene.collection.children_recursive if edit_text.lower() in c.name.lower()] + +class RT_OT_conform_collection_hierarchy(Operator): + bl_idname = "rt.conform_collection_hierarchy" + bl_label = "Conform Collection Hierarchy" + bl_description = "Chek and conform collection visibility hierarchy settings\ + \nCan affect collection, objects or both" + bl_options = {"REGISTER", "UNDO"} + + + hierarchy_type: EnumProperty( + name="Hierarchy Type", + description="Choose whether to conform object hierarchy or collection hierarchy", + items=[ + ('COLLECTION', "Collection Hierarchy", "Conform collection hierarchy") + ('OBJECT', "Object Hierarchy", "Conform object hierarchy"), + ], + default='COLLECTION' + ) + + target_collection: StringProperty( + name="Target Collecton", + description="Collection to target", # or object name # (useful for excluded collections) + default="", + search=collection_search_callback + ## basic collection fetch: + # search=lambda self, context, edit_text: [(c.name, '') for c in bpy.context.scene.collection.children_recursive if edit_text.lower() in c.name.lower()] + ) + + affect_target: EnumProperty( + name="Affect Target", + description="Choose whether to affect collections, objects, or both", + items=[ + ('COLLECTION', "Collection", "Affect collections only"), + ('OBJECT', "Object", "Affect objects only"), + ('BOTH', "Both", "Affect both collections and objects") + ], + default='BOTH' + ) + + ## Common object and collection + + conform_selectability: BoolProperty( + name="Hide Select State", + description="Conform hide select select", + default=True + ) + + conform_viewlayer: BoolProperty( + name="Hide in Viewlayer State", + description="Conform viewlayer temporary hide", + default=True + ) + + conform_viewport: BoolProperty( + name="Disable in Viewports State", + description="Conform the monitor icon (global viewport disable)", + default=True + ) + + conform_render: BoolProperty( + name="Disable in Renders State", + description="Conform the camera icon (render visibility)", + default=True + ) + + ## Specific to collections + + conform_exclude: BoolProperty( + name="Exclude View Layer State", + description="Conform the exclude from view layer", + default=True + ) + + conform_holdout: BoolProperty( + name="Holdout State", + description="Conform Collection Holdout State", + default=True + ) + + conform_use_indirect: BoolProperty( + name="Indirect Only State", + description="Conform Collection Indirect Only", + default=True + ) + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self, width=400) + + def draw(self, context): + layout = self.layout + + # if self.hierarchy_type == 'COLLECTION': + layout.prop(self, "Get Collection by name") + layout.prop(self, "affect_target", text="Target Items") + + # layout.prop(self, "target_name", text="Target Collection") + ## Works with 'children' (root hierarchy), but not with 'children_recursive' + # layout.prop_search(self, "target_name", bpy.context.scene.collection, "children", text="Target Collection") + + target_collection = None + if not self.target_collection: + target_collection = context.collection + return + + if not self.target_collection: + layout.label(text="Select a collection or search by name", icon='INFO') + return + + root_col = next((c for c in context.scene.collection.children_recursive if c.name == target_collection), None) + + if not root_col: + layout.label(text=f"Error: Collection '{target_collection}' not found", icon='ERROR') + return + + vlc_root = fn.get_view_layer_collection(root_col) + if not vlc_root: + layout.label(text=f"Error: Viewlayer Collection '{target_collection}' not found", icon='ERROR') + return + + ## TODO: Show current state of the selected root collection + col = layout.column(align=True) + + row = layout.row(align=True) + row.prop(vlc_root, "exclude", text="", emboss=False) + row.prop(root_col, "hide_select", text="", emboss=False) + row.prop(vlc_root, "hide_viewport", text="", emboss=False) + row.prop(root_col, "hide_viewport", text="", emboss=False) + row.prop(root_col, "hide_render", text="", emboss=False) + row.prop(vlc_root, "holdout", text="", emboss=False) + row.prop(vlc_root, "indirect_only", text="", emboss=False) + + col = layout.column(align=True) + col.label(text="Parameter To Conform:") + row = col.row(align=True) + ## Same order, greyout unused options + collec_row = row.row(align=True) + collec_row.prop(self, "conform_exclude", text="", icon='CHECKBOX_HLT') # Exclude from View Layer + collec_row.active = self.affect_target != 'OBJECT' + + ## Object and collections + row.prop(self, "conform_selectability", text="", icon='RESTRICT_SELECT_OFF') # Hide Select + row.prop(self, "conform_viewlayer", text="", icon='HIDE_OFF') # Hide in current viewlayer (eye) + row.prop(self, "conform_viewport", text="", icon='RESTRICT_VIEW_OFF') # Disable in Viewports + row.prop(self, "conform_render", text="", icon='RESTRICT_RENDER_OFF') # Disable in Renders + + ## Specific to collections + collec_row = row.row(align=True) + collec_row.prop(self, "conform_holdout", text="", icon='HOLDOUT_OFF') # Holdout + collec_row.prop(self, "conform_use_indirect", text="", icon='INDIRECT_ONLY_OFF') # Indirect Only + collec_row.active = self.affect_target != 'OBJECT' + + ## TODO: Show live wich object / collection are affected by the conformation action when executed. + + sub_vlc = fn.get_collection_children_recursive(vlc_root) + + def execute(self, context): + return {'FINISHED'} + +# endregion + + +def register(): + bpy.utils.register_class(RT_OT_conform_collection_hierarchy) + + +def unregister(): + bpy.utils.unregister_class(RT_OT_conform_collection_hierarchy) diff --git a/ui.py b/ui.py index 2060117..7ec59b3 100755 --- a/ui.py +++ b/ui.py @@ -50,37 +50,50 @@ class RT_PT_visibility_check_ui_node(RT_PT_visibility_check_ui_base): bl_region_type = 'UI' bl_category = "Render" # Wrangler ? -## Unused, only exposed in Create output panel -class RT_PT_output_template(Panel): - bl_space_type = "3D_VIEW" +class RT_PT_conformation_ui(bpy.types.Panel): + bl_space_type = "VIEW_3D" bl_region_type = "UI" - bl_category = "Render" # Wrangler - bl_label = "File Output Templates" - bl_parent_id = "RT_PT_render_toolbox_ui" - # bl_options = {'DEFAULT_CLOSED'} + bl_category = "View" + bl_label = "Conformation" + bl_options = {'DEFAULT_CLOSED'} def draw(self, context): layout = self.layout - settings = context.scene.render_toolbox - - col = layout.column(align=True) - col.label(text='Single file:') - col.prop(settings, "default_base_path", text="Base Path") - col.prop(settings, "default_file_slot", text="File Slot") - - col.separator() - col = layout.column(align=True) - col.label(text='Multilayers:') - col.prop(settings, "default_multilayer_base_path", text="Base Path") - col.prop(settings, "default_multilayer_name", text="Layer Name") + layout.operator("rt.conform_collection_hierarchy", + text="Conform Collection Hierarchy", icon="OUTLINER_COLLECTION") - ## Handle separate tech passes names ? +## Unused, only exposed in Create output panel +# class RT_PT_output_template(Panel): +# bl_space_type = "3D_VIEW" +# bl_region_type = "UI" +# bl_category = "Render" # Wrangler +# bl_label = "File Output Templates" +# bl_parent_id = "RT_PT_render_toolbox_ui" +# # bl_options = {'DEFAULT_CLOSED'} + +# def draw(self, context): +# layout = self.layout +# settings = context.scene.render_toolbox + +# col = layout.column(align=True) +# col.label(text='Single file:') +# col.prop(settings, "default_base_path", text="Base Path") +# col.prop(settings, "default_file_slot", text="File Slot") + +# col.separator() +# col = layout.column(align=True) +# col.label(text='Multilayers:') +# col.prop(settings, "default_multilayer_base_path", text="Base Path") +# col.prop(settings, "default_multilayer_name", text="Layer Name") + +# ## Handle separate tech passes names ? classes = ( RT_PT_render_toolbox_ui, RT_PT_visibility_check_ui_viewport, - RT_PT_visibility_check_ui_node + RT_PT_visibility_check_ui_node, + RT_PT_conformation_ui, # RT_PT_output_template, )