conform hierarchy : first working version for collection. UI wip

This commit is contained in:
pullusb 2025-07-24 16:55:06 +02:00
parent 22e97d63ca
commit e98300ac44
3 changed files with 264 additions and 74 deletions

View File

@ -2,7 +2,7 @@ bl_info = {
"name": "Render Toolbox", "name": "Render Toolbox",
"description": "Perform checks and setup outputs", "description": "Perform checks and setup outputs",
"author": "Samuel Bernou", "author": "Samuel Bernou",
"version": (0, 5, 1), "version": (0, 6, 0),
"blender": (4, 0, 0), "blender": (4, 0, 0),
"location": "View3D", "location": "View3D",
"warning": "", "warning": "",

12
fn.py
View File

@ -420,9 +420,9 @@ def show_and_active_object(obj, make_active=True, select=True, unhide=True):
if select: if select:
obj.select_set(True) 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 '''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: if sublist have 2 element:
considered a label [text, icon] considered a label [text, icon]
if sublist have 3 element: if sublist have 3 element:
@ -433,7 +433,7 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
for l in _message: for l in message:
if isinstance(l, str): if isinstance(l, str):
layout.label(text=l) layout.label(text=l)
elif len(l) == 2: # label with icon 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.label(text=l[2], icon=l[3])
row.prop(l[0], l[1], text='') row.prop(l[0], l[1], text='')
if isinstance(_message, str): if isinstance(message, str):
_message = [_message] message = [message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon)
def get_rightmost_number_in_string(string) -> str: def get_rightmost_number_in_string(string) -> str:

View File

@ -7,10 +7,26 @@ from bpy.props import (BoolProperty,
StringProperty) StringProperty)
from .. import fn from .. import fn
def collection_search_callback(self, context, edit_text): def name_search_callback(self, context, edit_text):
"""Search callback for collection names""" """Search callback for collection names"""
## second arg is not displayed, can be and empty string... ## 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): class RT_OT_conform_collection_hierarchy(Operator):
bl_idname = "rt.conform_collection_hierarchy" bl_idname = "rt.conform_collection_hierarchy"
@ -23,18 +39,29 @@ class RT_OT_conform_collection_hierarchy(Operator):
hierarchy_type: EnumProperty( hierarchy_type: EnumProperty(
name="Hierarchy Type", name="Hierarchy Type",
description="Choose whether to conform object hierarchy or collection hierarchy", description="Choose whether to conform object hierarchy or collection hierarchy",
items=[ items=(
('COLLECTION', "Collection Hierarchy", "Conform collection hierarchy") ('COLLECTION', "Collection Hierarchy", "Conform collection hierarchy"),
('OBJECT', "Object Hierarchy", "Conform object hierarchy"), ('OBJECT', "Object Hierarchy", "Conform object hierarchy"),
], ),
default='COLLECTION' default='COLLECTION'
) )
target_collection: StringProperty( ## Utility prop : expose and control view layer hide state for active object
name="Target Collecton", ## just used for the update (actual bool value means nothing)
description="Collection to target", # or object name # (useful for excluded collections) ## 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="", default="",
search=collection_search_callback search=name_search_callback
## basic collection fetch: ## 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()] # 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=[ items=[
('COLLECTION', "Collection", "Affect collections only"), ('COLLECTION', "Collection", "Affect collections only"),
('OBJECT', "Object", "Affect objects 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 ## Common object and collection
@ -99,46 +126,54 @@ class RT_OT_conform_collection_hierarchy(Operator):
def invoke(self, context, event): def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=400) 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): def draw(self, context):
layout = self.layout layout = self.layout
# layout.use_property_split = True
# if self.hierarchy_type == 'COLLECTION': # layout.prop(self, "hierarchy_type", text="Root Type", expand=True)
layout.prop(self, "Get Collection by name") layout.prop(self, "target_name", text="Search (Optional)")
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 self.hierarchy_type == 'COLLECTION':
if not self.target_collection: root_collection = self.get_target_collection(context)
target_collection = context.collection if not root_collection:
return
if not self.target_collection:
layout.label(text="Select a collection or search by name", icon='INFO') layout.label(text="Select a collection or search by name", icon='INFO')
return return
if not root_collection:
root_col = next((c for c in context.scene.collection.children_recursive if c.name == target_collection), None) layout.label(text=f"Error: Collection '{root_collection}' not found", icon='ERROR')
if not root_col:
layout.label(text=f"Error: Collection '{target_collection}' not found", icon='ERROR')
return return
vlc_root = fn.get_view_layer_collection(root_col) vlc_root = fn.get_view_layer_collection(root_collection)
if not vlc_root: if not vlc_root:
layout.label(text=f"Error: Viewlayer Collection '{target_collection}' not found", icon='ERROR') layout.label(text=f"Error: Viewlayer Collection '{root_collection}' not found", icon='ERROR')
return return
## TODO: Show current state of the selected root collection row = layout.row(align=True)
col = layout.column(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 = layout.row(align=True)
row.prop(vlc_root, "exclude", text="", emboss=False) row.prop(vlc_root, "exclude", text="", emboss=False)
row.prop(root_col, "hide_select", text="", emboss=False) row.prop(root_collection, "hide_select", text="", emboss=False)
row.prop(vlc_root, "hide_viewport", text="", emboss=False) row.prop(vlc_root, "hide_viewport", text="", emboss=False)
row.prop(root_col, "hide_viewport", text="", emboss=False) row.prop(root_collection, "hide_viewport", text="", emboss=False)
row.prop(root_col, "hide_render", text="", emboss=False) row.prop(root_collection, "hide_render", text="", emboss=False)
row.prop(vlc_root, "holdout", text="", emboss=False) row.prop(vlc_root, "holdout", text="", emboss=False)
row.prop(vlc_root, "indirect_only", text="", emboss=False) row.prop(vlc_root, "indirect_only", text="", emboss=False)
@ -162,11 +197,166 @@ class RT_OT_conform_collection_hierarchy(Operator):
collec_row.prop(self, "conform_use_indirect", text="", icon='INDIRECT_ONLY_OFF') # Indirect Only collec_row.prop(self, "conform_use_indirect", text="", icon='INDIRECT_ONLY_OFF') # Indirect Only
collec_row.active = self.affect_target != 'OBJECT' collec_row.active = self.affect_target != 'OBJECT'
## TODO: Show live wich object / collection are affected by the conformation action when executed. 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':
# 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'] = 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
# ## 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
# 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)]
# 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')
sub_vlc = fn.get_collection_children_recursive(vlc_root)
def execute(self, context): 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'} return {'FINISHED'}
# endregion # endregion