From e98300ac4429e2c956dfee7b35dde209d16a26c6 Mon Sep 17 00:00:00 2001 From: pullusb Date: Thu, 24 Jul 2025 16:55:06 +0200 Subject: [PATCH] conform hierarchy : first working version for collection. UI wip --- __init__.py | 2 +- fn.py | 12 +- operators/conform_collection_hierarchy.py | 324 +++++++++++++++++----- 3 files changed, 264 insertions(+), 74 deletions(-) diff --git a/__init__.py b/__init__.py index f6d050f..f0bd563 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, 5, 1), + "version": (0, 6, 0), "blender": (4, 0, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index 60a0877..6263f18 100755 --- a/fn.py +++ b/fn.py @@ -420,9 +420,9 @@ def show_and_active_object(obj, make_active=True, select=True, unhide=True): if select: obj.select_set(True) -def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): +def show_message_box(message = "", title = "Message Box", icon = 'INFO'): '''Show message box with element passed as string or list - if _message if a list of lists: + if message is a list of lists: if sublist have 2 element: considered a label [text, icon] if sublist have 3 element: @@ -433,7 +433,7 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def draw(self, context): layout = self.layout - for l in _message: + for l in message: if isinstance(l, str): layout.label(text=l) elif len(l) == 2: # label with icon @@ -446,9 +446,9 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): row.label(text=l[2], icon=l[3]) row.prop(l[0], l[1], text='') - if isinstance(_message, str): - _message = [_message] - bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) + if isinstance(message, str): + message = [message] + bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) def get_rightmost_number_in_string(string) -> str: diff --git a/operators/conform_collection_hierarchy.py b/operators/conform_collection_hierarchy.py index 55885ba..bcc0f95 100644 --- a/operators/conform_collection_hierarchy.py +++ b/operators/conform_collection_hierarchy.py @@ -7,10 +7,26 @@ from bpy.props import (BoolProperty, StringProperty) from .. import fn -def collection_search_callback(self, context, edit_text): +def name_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()] + 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" @@ -23,18 +39,29 @@ class RT_OT_conform_collection_hierarchy(Operator): hierarchy_type: EnumProperty( name="Hierarchy Type", description="Choose whether to conform object hierarchy or collection hierarchy", - items=[ - ('COLLECTION', "Collection Hierarchy", "Conform 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) + ## 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=collection_search_callback + 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()] ) @@ -45,9 +72,9 @@ class RT_OT_conform_collection_hierarchy(Operator): items=[ ('COLLECTION', "Collection", "Affect collections only"), ('OBJECT', "Object", "Affect objects only"), - ('BOTH', "Both", "Affect both collections and objects") + ('ALL', "All", "Affect both collections and objects") ], - default='BOTH' + default='ALL' ) ## Common object and collection @@ -99,74 +126,237 @@ class RT_OT_conform_collection_hierarchy(Operator): 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="Root Type", expand=True) + layout.prop(self, "target_name", text="Search (Optional)") + + + if self.hierarchy_type == 'COLLECTION': + root_collection = self.get_target_collection(context) + if not root_collection: + layout.label(text="Select a collection or search by name", icon='INFO') + return + if not root_collection: + layout.label(text=f"Error: Collection '{root_collection}' not found", icon='ERROR') + return + + vlc_root = fn.get_view_layer_collection(root_collection) + if not vlc_root: + layout.label(text=f"Error: Viewlayer Collection '{root_collection}' not found", icon='ERROR') + return + + row = layout.row(align=True) + row.label(text="", icon='TRIA_RIGHT') + row.label(text=root_collection.name, icon='OUTLINER_COLLECTION') + + layout.prop(self, "affect_target", text="Target Items") + + row = layout.row(align=True) + row.prop(vlc_root, "exclude", text="", emboss=False) + row.prop(root_collection, "hide_select", text="", emboss=False) + row.prop(vlc_root, "hide_viewport", text="", emboss=False) + row.prop(root_collection, "hide_viewport", text="", emboss=False) + row.prop(root_collection, "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' + + else: + target_object = self.get_target_object(context) + + if not target_object: + layout.label(text="Make object active or search by name", icon='INFO') + return + + row = layout.row(align=True) + row.label(text="", icon='TRIA_RIGHT') + row.label(text=target_object.name, icon='OBJECT_DATA') + + if not target_object.children_recursive: + layout.label(text="Object has no children", icon='ERROR') + return + + col = layout.column(align=True) + row.prop(target_object, "hide_select", text="", emboss=False) + row.prop(self, "active_object_viewlayer_hide", text="", icon='HIDE_ON' if target_object.hide_get() else 'HIDE_OFF', emboss=False) # hack + row.prop(target_object, "hide_viewport", text="", emboss=False) + row.prop(target_object, "hide_render", text="", emboss=False) + + col.label(text="Parameter To Conform:") + 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 + + + ## Show dynamically wich object/collection are affected by the current confo. # 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") + # to_conform_viewlayer = {} + # to_conform_collection = {} - target_collection = None - if not self.target_collection: - target_collection = context.collection - return + # 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'] = vlc_root.exclude + # if self.conform_viewlayer: + # to_conform_viewlayer['hide_viewport'] = vlc_root.hide_viewport + # if self.conform_holdout: + # to_conform_viewlayer['holdout'] = vlc_root.holdout + # if self.conform_use_indirect: + # to_conform_viewlayer['indirect_only'] = vlc_root.indirect_only - 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) + # ## collection props + # if self.conform_selectability: + # to_conform_collection['hide_select'] = vlc_root.collection.hide_select + # if self.conform_viewport: + # to_conform_collection['hide_viewport'] = vlc_root.collection.hide_viewport + # if self.conform_render: + # to_conform_collection['hide_render'] = vlc_root.collection.hide_render + - if not root_col: - layout.label(text=f"Error: Collection '{target_collection}' not found", icon='ERROR') - return + # col = layout.column(align=True) + # sub_vlc = fn.get_collection_children_recursive(vlc_root) + # 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)] - 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 + # 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') - ## 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): + if self.hierarchy_type == 'COLLECTION': + ref_collection = self.get_target_collection(context) + vlc_root = fn.get_view_layer_collection(ref_collection) + if not vlc_root: + self.report({'ERROR'}, f"View Layer Collection for '{ref_collection.name}' not found") + return {'CANCELLED'} + + affected_items = [] # List to log affected items + if self.affect_target in {'ALL', 'COLLECTION'}: + ## Apply on collection + sub_vlc = fn.get_collection_children_recursive(vlc_root) + for vlc in sub_vlc: + # Apply view layer collection properties + + if self.conform_exclude and vlc.exclude != vlc_root.exclude: + vlc.exclude = vlc_root.exclude + affected_items.append((vlc.name, "exclude", vlc_root.exclude)) + + if self.conform_viewlayer and vlc.hide_viewport != vlc_root.hide_viewport: + vlc.hide_viewport = vlc_root.hide_viewport + affected_items.append((vlc.name, "hide_viewport", vlc_root.hide_viewport)) + + if self.conform_holdout and vlc.holdout != vlc_root.holdout: + vlc.holdout = vlc_root.holdout + affected_items.append((vlc.name, "holdout", vlc_root.holdout)) + + if self.conform_use_indirect and vlc.indirect_only != vlc_root.indirect_only: + vlc.indirect_only = vlc_root.indirect_only + affected_items.append((vlc.name, "indirect_only", vlc_root.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() != vlc_root.hide_viewport: + obj.hide_set(vlc_root.hide_viewport) + affected_items.append((obj.name, "hide_viewlayer", vlc_root.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)) + + + # Log the affected items + message = [] + for item in affected_items: + line = f'{item[0]} : {item[1]} {item[2]}' + print(line) + message.append(line) + + # f" items in the hierarchy." + '\n'.join(message) + fn.show_message_box( + message=message, + title=f"Conformed {len(affected_items)} in hierarchy", + icon='INFO' + ) + return {'FINISHED'} # endregion