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",
"description": "Perform checks and setup outputs",
"author": "Samuel Bernou",
"version": (0, 5, 1),
"version": (0, 6, 0),
"blender": (4, 0, 0),
"location": "View3D",
"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:
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:

View File

@ -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