diff --git a/__init__.py b/__init__.py index 4c27b3e..0c2244b 100755 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "Render Toolbox", "description": "Perform checks and setup outputs", "author": "Samuel Bernou", - "version": (0, 6, 1), + "version": (0, 6, 3), "blender": (4, 0, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index c46eeae..45ce502 100755 --- a/fn.py +++ b/fn.py @@ -337,6 +337,19 @@ def get_view_layer_collection(col, vl_col=None, view_layer=None): if c is not None: return c +## Alternative implementation +# def get_view_layer_collection(collection, vl_col=None): +# """Find the view layer collection corresponding to a given collection""" +# if vl_col is None: +# vl_col = bpy.context.view_layer.layer_collection +# if vl_col.collection == collection: +# return vl_col +# for child in vl_col.children: +# found = get_view_layer_collection(collection, child) +# if found: +# return found +# return None + 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) @@ -544,7 +557,24 @@ def build_path_from_template(template: str, # endregion -# region visibility states store +# region visibility states + +## targets for conformation +def get_target_collection(target_name, context): + """Get the target collection based on active or the target name, None if nothing""" + if target_name: + return next((c for c in context.scene.collection.children_recursive if c.name == target_name), None) + else: + return context.collection + +def get_target_object(target_name, context): + """Get the target object based on active or the target name, None if nothing""" + if target_name: + return bpy.context.scene.objects.get(target_name) + else: + return context.object + + def store_visibility_states(collection=None): """Store visibility states of objects and collections @@ -560,7 +590,7 @@ def store_visibility_states(collection=None): 'collections': {}, 'view_layer_collections': {} } - + # Determine target viewlayer collection if collection is None: target_vl_collection = bpy.context.view_layer.layer_collection @@ -569,7 +599,7 @@ def store_visibility_states(collection=None): if target_vl_collection is None: # Collection not found in current viewlayer return visibility_states - + # Store visibility states by iterating through viewlayer collections def store_visibility_recursive(vl_col): # Get the actual collection from viewlayer collection @@ -581,14 +611,13 @@ def store_visibility_states(collection=None): 'hide_viewport': getattr(col, 'hide_viewport', False), 'hide_render': getattr(col, 'hide_render', False), } - + # Store viewlayer collection visibility states visibility_states['view_layer_collections'][vl_col.name] = { 'exclude': getattr(vl_col, 'exclude', False), 'hide_viewport': getattr(vl_col, 'hide_viewport', False), 'indirect_only': getattr(vl_col, 'indirect_only', False), 'holdout': getattr(vl_col, 'indirect_only', False), - } # Store objects in this collection @@ -600,7 +629,7 @@ def store_visibility_states(collection=None): 'hide_select': obj.hide_select, 'hide_viewlayer': obj.hide_get(), } - + # Recursively process children for child_vl_col in vl_col.children: store_visibility_recursive(child_vl_col) @@ -608,3 +637,126 @@ def store_visibility_states(collection=None): store_visibility_recursive(target_vl_collection) return visibility_states + + +def store_collection_states(collection, context=None): + '''Pass a colelction and store visibility state''' + context = context or bpy.context + + scene = context.scene + outliner_state = scene.get('outliner_state', {}) + ## By name + # if collection is None: + # collection = next((c for c in context.scene.collection.children_recursive if c.name == target_name), None) + state = store_visibility_states(collection) + key = collection.name if collection else "ALL" + outliner_state[key] = state + scene['outliner_state'] = outliner_state + # return ({'INFO'}, f"Stored visibility states for '{key}'") + + +def apply_collection_states(collection, context=None): + context = context or bpy.context + + scene = context.scene + outliner_state = scene.get('outliner_state', {}) + if collection is None: + # If no collection is passed, apply to all collections + collection = context.scene.collection + key = "ALL" + else: + key = collection.name + + state = outliner_state.get(key) + if not state: + return ({'WARNING'}, f"No stored state for '{key}'") + # return {'CANCELLED'} + + for obj_name, obj_state in state['objects'].items(): + # obj = context.scene.objects.get(obj_name) + obj = collection.all_objects.get(obj_name) + if obj: + obj.hide_viewport = obj_state.get('hide_viewport', False) + obj.hide_render = obj_state.get('hide_render', False) + obj.hide_select = obj_state.get('hide_select', False) + try: + obj.hide_set(obj_state.get('hide_viewlayer', False)) + except Exception: + pass + + for col_name, col_state in state['collections'].items(): + col = next((c for c in context.scene.collection.children_recursive if c.name == col_name), None) + if col: + col.hide_select = col_state.get('hide_select', False) + col.hide_viewport = col_state.get('hide_viewport', False) + col.hide_render = col_state.get('hide_render', False) + + for vlcol_name, vlcol_state in state['view_layer_collections'].items(): + col = next((c for c in context.scene.collection.children_recursive if c.name == vlcol_name), None) + vlcol = get_view_layer_collection(col, context.view_layer.layer_collection) + if vlcol: + vlcol.exclude = vlcol_state.get('exclude', False) + vlcol.hide_viewport = vlcol_state.get('hide_viewport', False) + vlcol.indirect_only = vlcol_state.get('indirect_only', False) + vlcol.holdout = vlcol_state.get('holdout', False) + + ## --- + + # # Restore object states + # for obj, obj_state in state.get('objects', {}).items(): + # # obj = bpy.data.objects.get(obj_name) + # obj.hide_viewport = obj_state.get('hide_viewport', False) + # obj.hide_render = obj_state.get('hide_render', False) + # obj.hide_select = obj_state.get('hide_select', False) + # try: + # obj.hide_set(obj_state.get('hide_viewlayer', False)) + # except Exception: + # pass + + # # Restore collection states + # for col, col_state in state.get('collections', {}).items(): + # # col = bpy.data.collections.get(col_name) + # col.hide_select = col_state.get('hide_select', False) + # col.hide_viewport = col_state.get('hide_viewport', False) + # col.hide_render = col_state.get('hide_render', False) + + # for vlcol, vlcol_state in state.get('view_layer_collections', {}).items(): + # vlcol.hide_select = vlcol_state.get('hide_select', False) + # vlcol.hide_viewport = vlcol_state.get('hide_viewport', False) + # vlcol.hide_render = vlcol_state.get('hide_render', False) + + + ### ---- + + # Restore view layer collection states + # def apply_vl_states(vl_col): + # vl_state = state.get('view_layer_collections', {}).get(vl_col.name) + # if vl_state: + # vl_col.exclude = vl_state.get('exclude', False) + # vl_col.hide_viewport = vl_state.get('hide_viewport', False) + # vl_col.indirect_only = vl_state.get('indirect_only', False) + # vl_col.holdout = vl_state.get('holdout', False) + # for child in vl_col.children: + # apply_vl_states(child) + + # if collection: + # vl_col = get_view_layer_collection(collection) + # if vl_col: + # apply_vl_states(vl_col) + # else: + # apply_vl_states(context.view_layer.layer_collection) + + +def delete_collection_states(collection_name, context=None): + """Delete stored visibility states for a collection or all if None""" + context = context or bpy.context + + scene = context.scene + outliner_state = scene.get('outliner_state', {}) + key = collection_name if collection_name else "ALL" + if key in outliner_state: + del outliner_state[key] + scene['outliner_state'] = outliner_state + return ({'INFO'}, f"Deleted visibility states for '{key}'") + else: + return ({'WARNING'}, f"No stored state for '{key}'") \ No newline at end of file diff --git a/operators/__init__.py b/operators/__init__.py index c8558a3..d156440 100755 --- a/operators/__init__.py +++ b/operators/__init__.py @@ -4,6 +4,7 @@ from . import ( outputs_search_and_replace, visibility_conflicts, simplify_conflicts, + store_visibility_states, conform_collection_hierarchy, scene_checker, ) @@ -14,6 +15,7 @@ mods = ( outputs_search_and_replace, visibility_conflicts, simplify_conflicts, + store_visibility_states, conform_collection_hierarchy, scene_checker, ) diff --git a/operators/conform_collection_hierarchy.py b/operators/conform_collection_hierarchy.py index 96bb839..b2a6711 100644 --- a/operators/conform_collection_hierarchy.py +++ b/operators/conform_collection_hierarchy.py @@ -5,6 +5,7 @@ from bpy.props import (BoolProperty, PointerProperty, CollectionProperty, StringProperty) + from .. import fn ## TODO : handle linked collection / object @@ -31,368 +32,262 @@ def toggle_viewlayer_hide_state(self, context): target_object.hide_set(not target_object.hide_get()) +def outliner_conform(apply=False, context=None): + """conform the outliner hierarchy of a target children based on scene properties""" + + context = context or bpy.context + props = context.view_layer.render_toolbox_conform + + ## Dict model (with object/collection items in one key, and list of strings for affected properties) + # affected_items = { + # 'objects': { + # obj: ['hide_viewlayer, hide_viewport'], + # obj_2: ['hide_viewlayer, hide_select'], + # }, + # 'collections': { + # vlc_1 : ['exclude','hide_viewlayer','holdout'], + # vlc_2 : ['exclude', 'holdout', 'hide_viewport'], + # } + # } + + hierarchy_type = props.hierarchy_type + target_name = props.target_name + affect_target = props.affect_target + conform_selectability = props.conform_selectability + conform_viewlayer = props.conform_viewlayer + conform_viewport = props.conform_viewport + conform_render = props.conform_render + conform_exclude = props.conform_exclude + conform_holdout = props.conform_holdout + conform_use_indirect = props.conform_use_indirect + + affected_items = { + 'objects': {}, + 'collections': {} + } + + if hierarchy_type == 'COLLECTION': + ref_collection = fn.get_target_collection(target_name, context) + ref_vlc = fn.get_view_layer_collection(ref_collection) + if not ref_vlc: + print({'ERROR'}, f"View Layer Collection for '{ref_collection.name}' not found") + return + # print(f'Conform parameters on collection hierarchy from {ref_vlc.name}') + + if affect_target in {'ALL', 'COLLECTION'}: + sub_vlc = fn.get_collection_children_recursive(ref_vlc) + for vlc in sub_vlc: + col_attrs = [] + if conform_exclude and vlc.exclude != ref_vlc.exclude: + if apply: vlc.exclude = ref_vlc.exclude + col_attrs.append("exclude") + if conform_viewlayer and vlc.hide_viewport != ref_vlc.hide_viewport: + if apply: vlc.hide_viewport = ref_vlc.hide_viewport + col_attrs.append("hide_viewlayer") + if conform_holdout and vlc.holdout != ref_vlc.holdout: + if apply: vlc.holdout = ref_vlc.holdout + col_attrs.append("holdout") + if conform_use_indirect and vlc.indirect_only != ref_vlc.indirect_only: + if apply: vlc.indirect_only = ref_vlc.indirect_only + col_attrs.append("indirect_only") + if conform_selectability and vlc.collection.hide_select != ref_collection.hide_select: + if apply: vlc.collection.hide_select = ref_collection.hide_select + col_attrs.append("hide_select") + if conform_viewport and vlc.collection.hide_viewport != ref_collection.hide_viewport: + if apply: vlc.collection.hide_viewport = ref_collection.hide_viewport + col_attrs.append("hide_viewport") + if conform_render and vlc.collection.hide_render != ref_collection.hide_render: + if apply: vlc.collection.hide_render = ref_collection.hide_render + col_attrs.append("hide_render") + if col_attrs: + affected_items['collections'][vlc] = col_attrs + + if affect_target in {'ALL', 'OBJECT'}: + for obj in ref_collection.all_objects: + obj_attrs = [] + if conform_selectability and obj.hide_select != ref_collection.hide_select: + if apply: obj.hide_select = ref_collection.hide_select + obj_attrs.append("hide_select") + if conform_viewlayer and obj.hide_get() != ref_vlc.hide_viewport: + if apply: obj.hide_set(ref_vlc.hide_viewport) + obj_attrs.append("hide_viewlayer") + if conform_viewport and obj.hide_viewport != ref_collection.hide_viewport: + if apply: obj.hide_viewport = ref_collection.hide_viewport + obj_attrs.append("hide_viewport") + if conform_render and obj.hide_render != ref_collection.hide_render: + if apply: obj.hide_render = ref_collection.hide_render + obj_attrs.append("hide_render") + if obj_attrs: + affected_items['objects'][obj] = obj_attrs + + else: + ref_object = fn.get_target_object(target_name, context) + if not ref_object: + print({'ERROR'}, "No target object found") + return + # print(f'Conform parameters on object childrens hierarchy from {ref_object.name}') + for obj in ref_object.children_recursive: + obj_attrs = [] + if conform_selectability and obj.hide_select != ref_object.hide_select: + if apply: obj.hide_select = ref_object.hide_select + obj_attrs.append("hide_select") + if conform_viewlayer and obj.hide_get() != ref_object.hide_get(): + if apply: obj.hide_set(ref_object.hide_get()) + obj_attrs.append("hide_viewlayer") + if conform_viewport and obj.hide_viewport != ref_object.hide_viewport: + if apply: obj.hide_viewport = ref_object.hide_viewport + obj_attrs.append("hide_viewport") + if conform_render and obj.hide_render != ref_object.hide_render: + if apply: obj.hide_render = ref_object.hide_render + obj_attrs.append("hide_render") + if obj_attrs: + affected_items['objects'][obj] = obj_attrs + + return affected_items + + 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\ + bl_description = "Conform collection/object hierarchy state.\ \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=True, - 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=False - ) - - 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=False - ) - - conform_render: BoolProperty( - name="Disable in Renders State", - description="Conform the camera icon (render visibility)", - default=False - ) - - ## 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 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 + props = context.view_layer.render_toolbox_conform - 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: + ## Check if ready to continue OK + if props.hierarchy_type == 'COLLECTION': + ref_collection = fn.get_target_collection(props.target_name, context) + self.ref_item = fn.get_view_layer_collection(ref_collection) + if not self.ref_item: 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}') + fn.store_collection_states(ref_collection, context=context) - 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.ref_item = fn.get_target_object(props.target_name, context) + if not self.ref_item: 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())) + self.affected_items_dict = outliner_conform(apply=False, context=context) - 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)) + ## Merge collection and viewlauer colleciton on the same keys by name + # merged_collection_dict = {} + # for k, v in self.affected_items_dict.items(): + # if isinstance(k, bpy.types.Object): + # merged_collection_dict[k] = v + # else: + # if k.name not in merged_collection_dict.keys(): + # merged_collection_dict[k.name] = [] + # merged_collection_dict[k.name].extend(v) - 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)) + # self.affected_items_dict = merged_collection_dict - # Log the affected items + return context.window_manager.invoke_props_dialog(self, width=400) + + def draw(self, context): + + layout = self.layout + # show non conform items + props = context.view_layer.render_toolbox_conform + + layout.label(text=f"Conform Hierarchy from '{self.ref_item.name}'", icon='OUTLINER_COLLECTION' if props.hierarchy_type == 'COLLECTION' else 'OBJECT_DATA') + ## show conform parameters + row = layout.row(align=True) + row.enabled = False # don't want to mes with source item here ! TODO : convert to labels + row.label(text="Conforming:") + if props.hierarchy_type == 'COLLECTION': + if props.conform_exclude: + row.prop(self.ref_item, 'exclude', text="", emboss=False) + if props.conform_viewlayer: + row.prop(self.ref_item, 'hide_viewport', text="", emboss=False) + if props.conform_holdout: + row.prop(self.ref_item, 'holdout', text="", emboss=False) + if props.conform_use_indirect: + row.prop(self.ref_item, 'indirect_only', text="", emboss=False) + ## collection props + if props.conform_selectability: + row.prop(self.ref_item.collection, 'hide_select', text="", emboss=False) + if props.conform_viewport: + row.prop(self.ref_item.collection, 'hide_viewport', text="", emboss=False) + if props.conform_render: + row.prop(self.ref_item.collection, 'hide_render', text="", emboss=False) + else: + ## Object + if props.conform_selectability: + row.prop(self.ref_item, 'hide_select', text="", emboss=False) + if props.conform_viewlayer: + row.label(text="", icon='HIDE_ON' if self.ref_item.hide_get() else 'HIDE_OFF') + if props.conform_viewport: + row.prop(self.ref_item, 'hide_viewport', text="", emboss=False) + if props.conform_render: + row.prop(self.ref_item, 'hide_render', text="", emboss=False) + + + layout.separator() + + outliner_state = context.scene.get('outliner_state', {}) + ## Should have been saved at invoke + if outliner_state.get(self.ref_item.name): + # layout.label(text=f"Stored visibility states for '{self.ref_item.name}'", icon='FILE_TICK') + layout.operator("rt.apply_visibility_states", text=f"Restore State: {self.ref_item.name}", icon='FILE_REFRESH').collection_name = self.ref_item.name + + layout.separator() + # vl_attr_list = ['exclude', 'hide_viewlayer', 'holdout', 'indirect_only'] + attr_list = ['hide_select', 'hide_viewport', 'hide_render'] + full_attr_list = ['exclude', 'hide_select', 'hide_viewlayer', 'hide_viewport', 'hide_render', 'holdout', 'indirect_only'] + + for item_type, type_dict in self.affected_items_dict.items(): + + for k, v in type_dict.items(): + if item_type == "objects": + icon = f'OUTLINER_OB_{k.type}' + name = k.name + else: + icon = 'OUTLINER_COLLECTION' + name = k.name + + row = layout.row(align=True) + row.label(text=name, icon=icon) + + for attr in full_attr_list: + if attr in v: + if attr == 'hide_viewlayer': + ## Special case for view_layer hide (hide_viewport for layer collection -- hide_get for object) + if item_type == "objects": + ## TODO: create a dynamic property to toggle object VL viz + row.label(text="", icon='HIDE_ON' if k.hide_get() else 'HIDE_OFF') # , emboss=False, toggle=True + else: + row.prop(k, 'hide_viewport', text="", emboss=False) + # row.prop(k, 'hide_viewport', text="", icon='HIDE_ON' if k.hide_viewport else 'HIDE_OFF', emboss=False) # , toggle=True + + + elif attr in attr_list and item_type == "collections": + ## accessed on collection object from vl_col + row.prop(k.collection, attr, text="", emboss=False) + else: + row.prop(k, attr, text="", emboss=False) + + else: + row.label(text="", icon='BLANK1') + + def execute(self, context): + # props = context.scene.render_toolbox_conform + + affected_items_dict = outliner_conform(apply=True, context=context) message = [] - for item in affected_items: - line = f'{item[0]} : {item[1]} {item[2]}' + for _, v in affected_items_dict.items(): + for item, attr_list in v.items(): + line = f'{item.name} : {", ".join(attr_list)}' print(line) message.append(line) - + fn.show_message_box( message=message, - title=f"Conformed {len(affected_items)} in hierarchy", + title=f"Conformed {len(affected_items_dict)} in hierarchy", icon='INFO' ) diff --git a/operators/store_visibility_states.py b/operators/store_visibility_states.py new file mode 100644 index 0000000..e2934fa --- /dev/null +++ b/operators/store_visibility_states.py @@ -0,0 +1,86 @@ +import bpy +from bpy.props import (BoolProperty, + EnumProperty, + PointerProperty, + CollectionProperty, + StringProperty) +from .. import fn + +class RT_OT_store_visibility_states(bpy.types.Operator): + """Store visibility states of objects and collections""" + bl_idname = "rt.store_visibility_states" + bl_label = "Store Visibility States" + bl_options = {'REGISTER', 'UNDO'} + + collection_name: StringProperty( + name="Collection Name", + description="Name of the collection to store viewlayer states of a collection childrens. Leave empty for all.", + default="" + ) + + def execute(self, context): + if self.collection_name == "ALL-SCENE-COLLECTION": + # Store scene collection (under key "ALL") + fn.store_collection_states(None, context=context) + return {'FINISHED'} + + # next((c for c in context.scene.collection.children_recursive if c.name == collection_name), None) + col = fn.get_target_collection(self.collection_name, context=context) + fn.store_collection_states(col, context=context) + return {'FINISHED'} + +class RT_OT_apply_visibility_states(bpy.types.Operator): + """Apply stored visibility states to objects and collections""" + bl_idname = "rt.apply_visibility_states" + bl_label = "Apply Visibility States" + bl_options = {'REGISTER', 'UNDO'} + + collection_name: StringProperty( + name="Collection Name", + description="Name of the collection to apply visibility states for. Leave empty for all.", + default="" + ) + + def execute(self, context): + if self.collection_name == "ALL-SCENE-COLLECTION": + # Apply scene collection (under key "ALL") + ret = fn.apply_collection_states(None, context=context) + if isinstance(ret, tuple): + self.report(ret[0], ret[1]) + return {'FINISHED'} + + # next((c for c in context.scene.collection.children_recursive if c.name == collection_name), None) + col = fn.get_target_collection(self.collection_name, context=context) + ret = fn.apply_collection_states(col, context=context) + if isinstance(ret, tuple): + self.report(ret[0], ret[1]) + return {'FINISHED'} + + +class RT_OT_delete_visibility_states(bpy.types.Operator): + """Delete stored visibility states from scene""" + bl_idname = "rt.delete_visibility_states" + bl_label = "Delete Visibility States" + bl_options = {'REGISTER', 'UNDO'} + + collection_name: StringProperty( + name="Collection Name", + description="Name of the collection to delete visibility states for. Leave empty for all.", + default="" + ) + + def execute(self, context): + fn.delete_collection_states(self.collection_name, context=context) + return {'FINISHED'} + + +def register(): + bpy.utils.register_class(RT_OT_store_visibility_states) + bpy.utils.register_class(RT_OT_apply_visibility_states) + bpy.utils.register_class(RT_OT_delete_visibility_states) + + +def unregister(): + bpy.utils.unregister_class(RT_OT_store_visibility_states) + bpy.utils.unregister_class(RT_OT_apply_visibility_states) + bpy.utils.unregister_class(RT_OT_delete_visibility_states) diff --git a/properties.py b/properties.py index 90a0bba..12940bb 100644 --- a/properties.py +++ b/properties.py @@ -2,9 +2,14 @@ import bpy import os from bpy.types import PropertyGroup -from bpy.props import StringProperty +from bpy.props import (BoolProperty, + EnumProperty, + PointerProperty, + CollectionProperty, + StringProperty) +# region template props class RT_PG_render_toolbox_props(PropertyGroup): default_base_path : StringProperty( name="Base Path Template", @@ -58,6 +63,110 @@ class RT_PG_render_toolbox_props(PropertyGroup): # # options={'SKIP_SAVE'} # ) +# endregion + +# region visibility states + +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()] + +class RT_PG_render_toolbox_conform_props(PropertyGroup): + 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=True, + # 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=False + ) + + 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=False + ) + + conform_render: BoolProperty( + name="Disable in Renders State", + description="Conform the camera icon (render visibility)", + default=False + ) + + ## 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 + ) + +# endregion + +# region reset template functions + def reset_scene_path_templates(): ## set default template from environment variable if available, or fallback to preferences defaults prefs = bpy.context.preferences.addons[__package__].preferences @@ -80,9 +189,13 @@ def reset_scene_path_templates(): else: bpy.context.scene.render_toolbox.default_multilayer_base_path = prefs.base_path_multilayer_template +# endregion + +# region register classes = ( RT_PG_render_toolbox_props, + RT_PG_render_toolbox_conform_props, ) def register(): @@ -90,7 +203,7 @@ def register(): bpy.utils.register_class(cls) bpy.types.Scene.render_toolbox = bpy.props.PointerProperty(type=RT_PG_render_toolbox_props) - # reset_scene_path_templates() # restricted context error when trying assignation here (probably need handler) + bpy.types.ViewLayer.render_toolbox_conform = bpy.props.PointerProperty(type=RT_PG_render_toolbox_conform_props) # not stored, no need to unregister def unregister(): del bpy.types.Scene.render_toolbox diff --git a/ui.py b/ui.py index 7ec59b3..5033837 100755 --- a/ui.py +++ b/ui.py @@ -1,6 +1,7 @@ import bpy from bpy.types import Panel +from . import fn class RT_PT_render_toolbox_ui(Panel): @@ -59,8 +60,150 @@ class RT_PT_conformation_ui(bpy.types.Panel): def draw(self, context): layout = self.layout - layout.operator("rt.conform_collection_hierarchy", - text="Conform Collection Hierarchy", icon="OUTLINER_COLLECTION") + + ## Show properties for outliner conformation + layout.use_property_split = True + layout.use_property_decorate = False + + # props = context.scene.render_toolbox_conform + props = context.view_layer.render_toolbox_conform + + col = layout.column(align=True) + col.prop(props, "hierarchy_type", text="Work On") # , expand=True + # col.separator() + col.prop(props, "target_name", text="Search") # (Optional) + + ## Show current target + box = layout.box() + tgt_row = box.row() + + if props.hierarchy_type == 'COLLECTION': + ref_collection = fn.get_target_collection(props.target_name, 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') + col.operator("rt.store_visibility_states", text='Store target State', icon="DISK_DRIVE") + + ## Show current collection state (behave badly when changed, should be tweaked before) + # col = layout.column(align=True) + row = tgt_row.row(align=True) + 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) + + layout.prop(props, "affect_target", text="Target Items") + layout.separator() + + 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(props, "conform_exclude", text="", icon='CHECKBOX_DEHLT' if ref_vlc.exclude else 'CHECKBOX_HLT') # Exclude from View Layer + collec_row.active = props.affect_target != 'OBJECT' + + ## Object and collections + row.prop(props, "conform_selectability", text="", icon='RESTRICT_SELECT_ON' if ref_collection.hide_select else 'RESTRICT_SELECT_OFF') # Hide Select + row.prop(props, "conform_viewlayer", text="", icon='HIDE_ON' if ref_vlc.hide_viewport else 'HIDE_OFF') # Hide in current viewlayer (eye) + row.prop(props, "conform_viewport", text="", icon='RESTRICT_VIEW_ON' if ref_collection.hide_viewport else 'RESTRICT_VIEW_OFF') # Disable in Viewports + row.prop(props, "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(props, "conform_holdout", text="", icon='HOLDOUT_OFF') # Holdout + collec_row.prop(props, "conform_use_indirect", text="", icon='INDIRECT_ONLY_OFF') # Indirect Only + collec_row.active = props.affect_target != 'OBJECT' + + else: + ref_obj = fn.get_target_object(props.target_name, 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: + tgt_row.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 = tgt_row.row(align=True) + # row.label(text="Reference Object State:") + row.prop(ref_obj, "hide_select", text="", emboss=False) + # row.prop(props, "active_object_viewlayer_hide", text="", icon='HIDE_ON' if ref_obj.hide_get() else 'HIDE_OFF', emboss=False) # hack + row.label(text="", icon='HIDE_ON' if ref_obj.hide_get() else 'HIDE_OFF') # hack + row.prop(ref_obj, "hide_viewport", text="", emboss=False) + row.prop(ref_obj, "hide_render", text="", emboss=False) + + layout.separator() + col = layout.column(align=True) + row = col.row(align=True) + row.label(text="Parameter 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 + row.prop(props, "conform_render", text="", icon='RESTRICT_RENDER_ON' if ref_obj.hide_render else 'RESTRICT_RENDER_OFF') # Disable in Renders + + + layout.operator("rt.conform_collection_hierarchy",text="Conform Hierarchy", icon="CHECKMARK") + + +class RT_PT_outliner_state_ui(bpy.types.Panel): + bl_space_type = "VIEW_3D" + bl_region_type = "UI" + bl_category = "View" + bl_label = "Outliner State" + bl_options = {'DEFAULT_CLOSED'} + bl_parent_id = "RT_PT_conformation_ui" + + def draw(self, context): + layout = self.layout + ## Outliner state + + ## Scene + col = layout.column() + col.operator("rt.store_visibility_states", text='Store Whole Viewlayer State', icon="DISK_DRIVE").collection_name = "ALL-SCENE-COLLECTION" + stored_keys = context.scene.get('outliner_state', {}).keys() + if stored_keys and 'ALL' in stored_keys: + row = col.row(align=True) + row.operator("rt.apply_visibility_states", text=f"Restore Viewlayer State", icon="OUTLINER").collection_name = "ALL-SCENE-COLLECTION" + row.operator("rt.delete_visibility_states", text="", icon="TRASH").collection_name = 'ALL' + + col = layout.column() + ## Specific collection + if stored_keys: + col.label(text="Collections State:") + for key in stored_keys: + if key == 'ALL': + continue + row = col.row(align=True) + row.operator("rt.apply_visibility_states", text=f"Restore: {key}", icon="OUTLINER").collection_name = key + row.operator("rt.delete_visibility_states", text="", icon="TRASH").collection_name = key ## Unused, only exposed in Create output panel # class RT_PT_output_template(Panel): @@ -94,6 +237,7 @@ classes = ( RT_PT_visibility_check_ui_viewport, RT_PT_visibility_check_ui_node, RT_PT_conformation_ui, + RT_PT_outliner_state_ui, # RT_PT_output_template, )