import bpy from bpy.types import Operator from bpy.props import (BoolProperty, EnumProperty, PointerProperty, CollectionProperty, StringProperty) from .. import fn ## TODO : handle linked collection / object ## add properties to choose if linked/overidden object/collec should be used def name_search_callback(self, context, edit_text): """Search callback for collection names""" ## second arg is not displayed, can be and empty string... if self.hierarchy_type == 'COLLECTION': return [(c.name, '') for c in bpy.context.scene.collection.children_recursive if edit_text.lower() in c.name.lower()] else: return [(o.name, '') for o in bpy.context.scene.collection.all_objects if edit_text.lower() in o.name.lower()] def toggle_viewlayer_hide_state(self, context): target_object = None if self.target_name: target_object = bpy.data.objects.get(self.target_name) else: target_object = context.object if target_object: # toggle the view layer hide state target_object.hide_set(not target_object.hide_get()) 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' ) ## Utility prop : expose and control view layer hide state for active object ## just used for the update (actual bool value means nothing) ## can also use RT_PG_object_visibility from vis_conflict ops ## TODO: make an ops for this instead ? active_object_viewlayer_hide : BoolProperty( name="Active Object View Layer Hide", description="show / hide active object in current viewlayer", default=False, update=toggle_viewlayer_hide_state ) target_name: StringProperty( name="Root Item", description="Collection or object to target", # or object name # (useful for excluded collections) default="", search=name_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"), ('ALL', "All", "Affect both collections and objects") ], default='ALL' ) ## 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=False ) conform_holdout: BoolProperty( name="Holdout State", description="Conform Collection Holdout State", default=False ) conform_use_indirect: BoolProperty( name="Indirect Only State", description="Conform Collection Indirect Only", default=False ) def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=400) def get_target_collection(self, context): """Get the target collection based on active or the target name, None if nothing""" if self.target_name: return next((c for c in context.scene.collection.children_recursive if c.name == self.target_name), None) else: return context.collection def get_target_object(self, context): """Get the target object based on active or the target name, None if nothing""" if self.target_name: return bpy.context.scene.objects.get(self.target_name) else: return context.object def draw(self, context): layout = self.layout # layout.use_property_split = True layout.prop(self, "hierarchy_type", text="Work On") # , expand=True layout.separator() tgt_row = layout.row() layout.prop(self, "target_name", text="Search (Optional)") if self.hierarchy_type == 'COLLECTION': ref_collection = self.get_target_collection(context) if not ref_collection or ref_collection == context.scene.collection: layout.label(text="Select a collection or search by name", icon='INFO') if ref_collection == context.scene.collection: layout.label(text="Cannot use the scene collection", icon='ERROR') layout.label(text="An excluded collection collection cannot be active (use search)") return if not ref_collection: layout.label(text=f"Error: Collection '{ref_collection.name}' not found", icon='ERROR') return ref_vlc = fn.get_view_layer_collection(ref_collection) if not ref_vlc: layout.label(text=f"Error: Viewlayer Collection '{ref_collection.name}' not found", icon='ERROR') return # tgt_row = layout.row(align=True) tgt_row.label(text="", icon='TRIA_RIGHT') tgt_row.label(text=ref_collection.name, icon='OUTLINER_COLLECTION') layout.prop(self, "affect_target", text="Target Items") layout.separator() ## Show current collection state (behave badly when changed, should be tweaked before) # col = layout.column(align=True) # row = col.row(align=True) # row.label(text="Reference Collection State:") # row.prop(ref_vlc, "exclude", text="", emboss=False) # row.prop(ref_collection, "hide_select", text="", emboss=False) # row.prop(ref_vlc, "hide_viewport", text="", emboss=False) # row.prop(ref_collection, "hide_viewport", text="", emboss=False) # row.prop(ref_collection, "hide_render", text="", emboss=False) # row.prop(ref_vlc, "holdout", text="", emboss=False) # row.prop(ref_vlc, "indirect_only", text="", emboss=False) col = layout.column(align=True) row = col.row(align=True) row.label(text="Parameter To Conform:") ## Same order, greyout unused options collec_row = row.row(align=True) collec_row.prop(self, "conform_exclude", text="", icon='CHECKBOX_DEHLT' if ref_vlc.exclude else '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_ON' if ref_collection.hide_select else 'RESTRICT_SELECT_OFF') # Hide Select row.prop(self, "conform_viewlayer", text="", icon='HIDE_ON' if ref_vlc.hide_viewport else 'HIDE_OFF') # Hide in current viewlayer (eye) row.prop(self, "conform_viewport", text="", icon='RESTRICT_VIEW_ON' if ref_collection.hide_viewport else 'RESTRICT_VIEW_OFF') # Disable in Viewports row.prop(self, "conform_render", text="", icon='RESTRICT_RENDER_ON' if ref_collection.hide_render else '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' else: ref_obj = self.get_target_object(context) if not ref_obj: layout.label(text="Make object active or search by name", icon='INFO') return # tgt_row = layout.row(align=True) tgt_row.label(text="", icon='TRIA_RIGHT') tgt_row.label(text=ref_obj.name, icon='OBJECT_DATA') if not ref_obj.children_recursive: layout.label(text="Object has no children", icon='ERROR') return layout.separator() ## Show current collection state (can behave badly when changed, should be tweaked before) # col = layout.column(align=True) # row = col.row(align=True) # row.label(text="Reference Object State:") # row.prop(ref_obj, "hide_select", text="", emboss=False) # row.prop(self, "active_object_viewlayer_hide", text="", icon='HIDE_ON' if ref_obj.hide_get() else 'HIDE_OFF', emboss=False) # hack # row.prop(ref_obj, "hide_viewport", text="", emboss=False) # row.prop(ref_obj, "hide_render", text="", emboss=False) col = layout.column(align=True) row = col.row(align=True) row.label(text="Parameter To Conform:") row.prop(self, "conform_selectability", text="", icon='RESTRICT_SELECT_ON' if ref_obj.hide_select else 'RESTRICT_SELECT_OFF') # Hide Select row.prop(self, "conform_viewlayer", text="", icon='HIDE_ON' if ref_obj.hide_get() else 'HIDE_OFF') # Hide in current viewlayer (eye) row.prop(self, "conform_viewport", text="", icon='RESTRICT_VIEW_ON' if ref_obj.hide_viewport else 'RESTRICT_VIEW_OFF') # Disable in Viewports row.prop(self, "conform_render", text="", icon='RESTRICT_RENDER_ON' if ref_obj.hide_render else 'RESTRICT_RENDER_OFF') # Disable in Renders ## Show dynamically wich object/collection are affected by the current confo. # if self.hierarchy_type == 'COLLECTION': # to_conform_viewlayer = {} # to_conform_collection = {} # vl_name_list = ['exclude', 'hide_viewport', 'holdout', 'indirect_only'] # collec_name_list = ['hide_select', 'hide_viewport', 'hide_render'] # ## VL props # if self.conform_exclude: # to_conform_viewlayer['exclude'] = ref_vlc.exclude # if self.conform_viewlayer: # to_conform_viewlayer['hide_viewport'] = ref_vlc.hide_viewport # if self.conform_holdout: # to_conform_viewlayer['holdout'] = ref_vlc.holdout # if self.conform_use_indirect: # to_conform_viewlayer['indirect_only'] = ref_vlc.indirect_only # ## collection props # if self.conform_selectability: # to_conform_collection['hide_select'] = ref_vlc.collection.hide_select # if self.conform_viewport: # to_conform_collection['hide_viewport'] = ref_vlc.collection.hide_viewport # if self.conform_render: # to_conform_collection['hide_render'] = ref_vlc.collection.hide_render # col = layout.column(align=True) # sub_vlc = fn.get_collection_children_recursive(ref_vlc) # for vlc in sub_vlc: # viewlayer_conflicts = [attr for attr, value in to_conform_viewlayer.items() if value != getattr(vlc, attr, None)] # collection_conflicts = [attr for attr, value in to_conform_collection.items() if value != getattr(vlc.collection, attr, None)] # if viewlayer_conflicts or collection_conflicts: # row = col.row(align=True) # row.label(text=f"{vlc.name}:", icon='OUTLINER_COLLECTION') # for attr in vl_name_list: # if attr in viewlayer_conflicts: # row.prop(vlc, attr, text="", emboss=False) # else: # row.label(text="", icon='BLANK1') # # subrow = row.row(align=True) # # subrow.prop(vlc, attr, text="", emboss=False) # # subrow.enabled = False # for attr in collec_name_list: # if attr in collection_conflicts: # row.prop(vlc.collection, attr, text="", emboss=False) # else: # row.label(text="", icon='BLANK1') def execute(self, context): affected_items = [] # List to log affected items if self.hierarchy_type == 'COLLECTION': ref_collection = self.get_target_collection(context) ref_vlc = fn.get_view_layer_collection(ref_collection) if not ref_vlc: self.report({'ERROR'}, f"View Layer Collection for '{ref_collection.name}' not found") return {'CANCELLED'} print(f'Conform parameters on collection hierarchy from {ref_vlc.name}') if self.affect_target in {'ALL', 'COLLECTION'}: ## Apply on collection sub_vlc = fn.get_collection_children_recursive(ref_vlc) for vlc in sub_vlc: # Apply view layer collection properties if self.conform_exclude and vlc.exclude != ref_vlc.exclude: vlc.exclude = ref_vlc.exclude affected_items.append((vlc.name, "exclude", ref_vlc.exclude)) if self.conform_viewlayer and vlc.hide_viewport != ref_vlc.hide_viewport: vlc.hide_viewport = ref_vlc.hide_viewport affected_items.append((vlc.name, "hide_viewport", ref_vlc.hide_viewport)) if self.conform_holdout and vlc.holdout != ref_vlc.holdout: vlc.holdout = ref_vlc.holdout affected_items.append((vlc.name, "holdout", ref_vlc.holdout)) if self.conform_use_indirect and vlc.indirect_only != ref_vlc.indirect_only: vlc.indirect_only = ref_vlc.indirect_only affected_items.append((vlc.name, "indirect_only", ref_vlc.indirect_only)) # Apply collection properties if self.conform_selectability and vlc.collection.hide_select != ref_collection.hide_select: vlc.collection.hide_select = ref_collection.hide_select affected_items.append((vlc.collection.name, "hide_select", ref_collection.hide_select)) if self.conform_viewport and vlc.collection.hide_viewport != ref_collection.hide_viewport: vlc.collection.hide_viewport = ref_collection.hide_viewport affected_items.append((vlc.collection.name, "hide_viewport", ref_collection.hide_viewport)) if self.conform_render and vlc.collection.hide_render != ref_collection.hide_render: vlc.collection.hide_render = ref_collection.hide_render affected_items.append((vlc.collection.name, "hide_render", ref_collection.hide_render)) if self.affect_target in {'ALL', 'OBJECT'}: ## Apply on objects for obj in ref_collection.all_objects: # Apply object properties if self.conform_selectability and obj.hide_select != ref_collection.hide_select: obj.hide_select = ref_collection.hide_select affected_items.append((obj.name, "hide_select", ref_collection.hide_select)) if self.conform_viewlayer and obj.hide_get() != ref_vlc.hide_viewport: obj.hide_set(ref_vlc.hide_viewport) affected_items.append((obj.name, "hide_viewlayer", ref_vlc.hide_viewport)) if self.conform_viewport and obj.hide_viewport != ref_collection.hide_viewport: obj.hide_viewport = ref_collection.hide_viewport affected_items.append((obj.name, "hide_viewport", ref_collection.hide_viewport)) if self.conform_render and obj.hide_render != ref_collection.hide_render: obj.hide_render = ref_collection.hide_render affected_items.append((obj.name, "hide_render", ref_collection.hide_render)) else: ref_object = self.get_target_object(context) if not ref_object: self.report({'ERROR'}, "No target object found") return {'CANCELLED'} print(f'Conform parameters on object childrens hierarchy from {ref_object.name}') for obj in ref_object.children_recursive: # Apply object properties if self.conform_selectability and obj.hide_select != ref_object.hide_select: obj.hide_select = ref_object.hide_select affected_items.append((obj.name, "hide_select", ref_object.hide_select)) if self.conform_viewlayer and obj.hide_get() != ref_object.hide_get(): obj.hide_set(ref_object.hide_get()) affected_items.append((obj.name, "hide_viewlayer", ref_object.hide_get())) if self.conform_viewport and obj.hide_viewport != ref_object.hide_viewport: obj.hide_viewport = ref_object.hide_viewport affected_items.append((obj.name, "hide_viewport", ref_object.hide_viewport)) if self.conform_render and obj.hide_render != ref_object.hide_render: obj.hide_render = ref_object.hide_render affected_items.append((obj.name, "hide_render", ref_object.hide_render)) # Log the affected items message = [] for item in affected_items: line = f'{item[0]} : {item[1]} {item[2]}' print(line) message.append(line) fn.show_message_box( message=message, title=f"Conformed {len(affected_items)} in hierarchy", icon='INFO' ) 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)