render_toolbox/operators/visibility_conflicts.py

503 lines
19 KiB
Python

import bpy
from bpy.types import Operator
from bpy.props import (BoolProperty,
EnumProperty,
PointerProperty,
CollectionProperty,
StringProperty)
# region Object visibility
class RT_OT_sync_visibility(Operator):
bl_idname = "rt.sync_visibility"
bl_label = "Sync Visibility"
bl_description = "Sync visibility properties with optional locking"
bl_options = {"REGISTER", "UNDO"}
sync_mode: EnumProperty(
name="Sync From",
description="Choose which visibility property to sync from",
items=[
('FROM_VIEWLAYER', "Viewlayer", "Use viewlayer visibility as source"),
('FROM_VIEWPORT', "Viewport", "Use viewport visibility as source"),
('FROM_RENDER', "Render", "Use render visibility as source"),
('VISIBLE_TO_RENDER', "Overall Visible", "Use overall viewport visibility (combination of viewport + viewlayer)")
],
default='FROM_VIEWLAYER'
)
affect_viewlayer: BoolProperty(
name="Affect Viewlayer",
description="Update viewlayer visibility",
default=True
)
affect_viewport: BoolProperty(
name="Affect Viewport",
description="Update viewport visibility",
default=True
)
affect_render: BoolProperty(
name="Affect Render",
description="Update render visibility",
default=True
)
popup: BoolProperty(
name="Popup",
description="Show this operator as a popup dialog, else directly call execute",
default=True,
options={'HIDDEN'}
)
def invoke(self, context, event):
if not self.popup:
# If not a popup, just execute directly
return self.execute(context)
# Auto-disable the source property to avoid self-sync
if self.sync_mode == 'FROM_VIEWLAYER':
self.affect_viewlayer = False
elif self.sync_mode == 'FROM_VIEWPORT':
self.affect_viewport = False
elif self.sync_mode == 'FROM_RENDER':
self.affect_render = False
elif self.sync_mode == 'VISIBLE_TO_RENDER':
# Only render makes sense for this mode
self.affect_viewlayer = False
self.affect_viewport = False
self.affect_render = True
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "sync_mode")
layout.separator()
# Target selection
col = layout.column(align=True)
col.label(text="Affect Properties:")
if self.sync_mode == 'VISIBLE_TO_RENDER':
# For this mode, only render makes sense
col.prop(self, "affect_render")
if not self.affect_render:
col.label(text="No targets selected", icon='ERROR')
else:
col.prop(self, "affect_viewlayer")
col.prop(self, "affect_viewport")
col.prop(self, "affect_render")
# Show warning if no targets selected
if not any([self.affect_viewlayer, self.affect_viewport, self.affect_render]):
col.label(text="No targets selected", icon='ERROR')
layout.separator()
# Show info about what will be affected
if self.sync_mode == 'VISIBLE_TO_RENDER':
if self.affect_render:
layout.label(text="Will update: Render visibility", icon='INFO')
else:
layout.label(text="Nothing will change", icon='INFO')
else:
affected = []
if self.affect_viewlayer:
affected.append("Viewlayer")
if self.affect_viewport:
affected.append("Viewport")
if self.affect_render:
affected.append("Render")
if affected:
layout.label(text=f"Will update: {', '.join(affected)}", icon='INFO')
else:
layout.label(text="Nothing will change", icon='INFO')
def execute(self, context):
if not self.popup:
# Always
if self.sync_mode == 'FROM_VIEWLAYER':
self.affect_viewlayer = False
elif self.sync_mode == 'FROM_VIEWPORT':
self.affect_viewport = False
elif self.sync_mode == 'FROM_RENDER':
self.affect_render = False
elif self.sync_mode == 'VISIBLE_TO_RENDER':
# Only render makes sense for this mode
self.affect_viewlayer = False
self.affect_viewport = False
self.affect_render = True
for obj in context.scene.objects:
# Get source visibility value
if self.sync_mode == 'FROM_VIEWLAYER':
source_hidden = obj.hide_get()
elif self.sync_mode == 'FROM_VIEWPORT':
source_hidden = obj.hide_viewport
elif self.sync_mode == 'FROM_RENDER':
source_hidden = obj.hide_render
elif self.sync_mode == 'VISIBLE_TO_RENDER':
# For this mode, we use the inverse of visible_get()
source_hidden = not obj.visible_get()
# Apply to selected target properties
if self.sync_mode == 'VISIBLE_TO_RENDER':
# Special case: only affects render visibility
if self.affect_render:
obj.hide_render = source_hidden
else:
# Standard sync modes: apply to selected properties
if self.affect_viewlayer:
obj.hide_set(source_hidden)
if self.affect_viewport:
obj.hide_viewport = source_hidden
if self.affect_render:
obj.hide_render = source_hidden
return {'FINISHED'}
class RT_PG_object_visibility(bpy.types.PropertyGroup):
"""Property group to handle object visibility"""
is_hidden: BoolProperty(
name="Hide in Viewport",
description="Toggle object visibility in viewport",
get=lambda self: self.get("is_hidden", False),
set=lambda self, value: self.set_visibility(value)
)
object_name: StringProperty(name="Object Name")
def set_visibility(self, value):
"""Set the visibility using hide_set()"""
obj = bpy.context.view_layer.objects.get(self.object_name)
if obj:
obj.hide_set(value)
self["is_hidden"] = value
class RT_OT_list_object_visibility_conflicts(Operator):
bl_idname = "rt.list_object_visibility_conflicts"
bl_label = "List Objects Visibility Conflicts"
bl_description = "List objects visibility conflicts.\
\nWhen Viewlayer, viewport and render have different values\
\nAlso allow to set all from one of the 3"
bl_options = {"REGISTER"}
visibility_items: CollectionProperty(type=RT_PG_object_visibility)
affect_viewlayer: BoolProperty(
name="Affect Viewlayer",
description="Update viewlayer visibility",
default=True
)
affect_viewport: BoolProperty(
name="Affect Viewport",
description="Update viewport visibility",
default=True
)
affect_render: BoolProperty(
name="Affect Render",
description="Update render visibility",
default=True
)
def invoke(self, context, event):
# Clear and rebuild both collections
self.visibility_items.clear()
# Store objects with conflicts
## TODO: Maybe better (but less detailed) to just check o.visible_get (global visiblity) against render viz ?
objects_with_conflicts = [o for o in context.scene.objects if not (o.hide_get() == o.hide_viewport == o.hide_render)]
# Create visibility items in same order
for obj in objects_with_conflicts:
item = self.visibility_items.add()
item.object_name = obj.name
item["is_hidden"] = obj.hide_get()
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
layout = self.layout
col = layout.column(align=True)
row = col.row(align=False)
row.label(text="Affect Visibility On:")
row.prop(self, "affect_viewlayer", text="", icon='CHECKBOX_HLT' if self.affect_viewlayer else 'CHECKBOX_DEHLT') # Viewlayer
row.prop(self, "affect_viewport", text="", icon='CHECKBOX_HLT' if self.affect_viewport else 'CHECKBOX_DEHLT') # Viewport
row.prop(self, "affect_render", text="", icon='CHECKBOX_HLT' if self.affect_render else 'CHECKBOX_DEHLT') # Render
if not any([self.affect_viewlayer, self.affect_viewport, self.affect_render]):
col.label(text="Need to select one target", icon='ERROR')
# Add sync buttons at the top
row = col.row(align=False)
row.label(text="Set Visibility State From:")
row_vl = row.row(align=True)
row_vl.active = self.affect_viewlayer
op = row_vl.operator("rt.sync_visibility", text="", icon='HIDE_OFF')
op.sync_mode = 'FROM_VIEWLAYER'
op.affect_viewlayer = self.affect_viewlayer
op.affect_viewport = self.affect_viewport
op.affect_render = self.affect_render
op.popup = False
row_vp = row.row(align=True)
row_vp.active = self.affect_viewport
op = row_vp.operator("rt.sync_visibility", text="", icon='RESTRICT_VIEW_OFF')
op.sync_mode = 'FROM_VIEWPORT'
op.affect_viewlayer = self.affect_viewlayer
op.affect_viewport = self.affect_viewport
op.affect_render = self.affect_render
op.popup = False
row_rd = row.row(align=True)
row_rd.active = self.affect_render
op = row_rd.operator("rt.sync_visibility", text="", icon='RESTRICT_RENDER_OFF')
op.sync_mode = 'FROM_RENDER'
op.affect_viewlayer = self.affect_viewlayer
op.affect_viewport = self.affect_viewport
op.affect_render = self.affect_render
op.popup = False
## Add that in a separate view mode
col.separator()
op = col.operator("rt.sync_visibility", text="Set Render state from current visibility", icon='RESTRICT_RENDER_OFF')
op.sync_mode = 'VISIBLE_TO_RENDER'
op.popup = False
layout.separator()
col = layout.column()
# We can safely iterate over visibility_items since objects are stored in same order
for vis_item in self.visibility_items:
obj = context.view_layer.objects.get(vis_item.object_name)
if not obj:
continue
row = col.row(align=False)
row.label(text=obj.name)
## Viewlayer visibility "as prop" to allow slide toggle
# hide_icon='HIDE_ON' if vis_item.is_hidden else 'HIDE_OFF'
row_vl = row.row(align=True)
row_vl.enabled = self.affect_viewlayer
hide_icon='HIDE_ON' if obj.hide_get() else 'HIDE_OFF' # based on object state
row_vl.prop(vis_item, "is_hidden", text="", icon=hide_icon, emboss=False)
# Direct object properties
row_vp = row.row(align=True)
row_vp.enabled = self.affect_viewport
row_vp.prop(obj, 'hide_viewport', text='', emboss=False)
row_rd = row.row(align=True)
row_rd.enabled = self.affect_render
row_rd.prop(obj, 'hide_render', text='', emboss=False)
def execute(self, context):
return {'FINISHED'}
## Basic version with only viewport and render visibility listed
class RT_OT_list_viewport_render_visibility(Operator):
bl_idname = "rt.list_viewport_render_visibility"
bl_label = "List Viewport And Render Visibility Conflicts"
bl_description = "List objects visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
def invoke(self, context, event):
self.ob_list = [o for o in context.scene.objects if o.hide_viewport != o.hide_render]
return context.window_manager.invoke_props_dialog(self, width=250)
def draw(self, context):
# TODO: Add visibility check with viewlayer visibility as well
layout = self.layout
for o in self.ob_list:
row = layout.row()
row.label(text=o.name)
row.prop(o, 'hide_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(o, 'hide_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
return {'FINISHED'}
# endregion
# region Collection Visibility
def get_collection_children_recursive(col, cols=None) -> list:
'''return a list of all the child collections
and their subcollections in the passed collection'''
cols = cols or []
for sub in col.children:
if sub not in cols:
cols.append(sub)
if len(sub.children):
cols = get_collection_children_recursive(sub, cols)
return cols
def get_viewlayer_collections_with_visiblity_conflict(context):
'''return viewlayer collections with visibility conflicts between hide in viewlayer, hide viewport and hide render'''
vcols = get_collection_children_recursive(context.view_layer.layer_collection)
vcols = list(set(vcols)) # ensure no duplicates
## Store collection with conflicts
return [vc for vc in vcols if not (vc.hide_viewport == vc.collection.hide_viewport == vc.collection.hide_render)]
class RT_OT_list_collection_visibility_conflicts(Operator):
bl_idname = "rt.list_collection_visibility_conflicts"
bl_label = "List Collection Visibility Conflicts"
bl_description = "List collection visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
# visibility_items: CollectionProperty(type=RT_PG_collection_visibility)
show_filter : bpy.props.EnumProperty(
name="View Filter",
description="Filter collections based on their exclusion status",
items=(
('ALL', "All", "Show all collections", 0),
('NOT_EXCLUDED', "Not Excluded", "Show collections that are not excluded", 1),
('EXCLUDED', "Excluded", "Show collections that are excluded", 2)
),
default='NOT_EXCLUDED')
def invoke(self, context, event):
## get all viewlayer collections
self.conflict_collections = get_viewlayer_collections_with_visiblity_conflict(context)
vcols = get_collection_children_recursive(context.view_layer.layer_collection)
vcols = list(set(vcols)) # ensure no duplicates
## Store collection with conflicts
self.conflict_collections = [vc for vc in vcols if not (vc.hide_viewport == vc.collection.hide_viewport == vc.collection.hide_render)]
self.included_collection = [vc for vc in self.conflict_collections if not vc.exclude]
self.excluded_collection = [vc for vc in self.conflict_collections if vc.exclude]
return context.window_manager.invoke_props_dialog(self, width=274)
def draw(self, context):
layout = self.layout
layout.prop(self, 'show_filter', expand=True)
# Add sync buttons at the top
row = layout.row(align=False)
# TODO: Add "set all from" ops on collection (optionnal)
# row.label(text="Sync All Visibility From:")
# row.operator("rt.sync_visibility_from_viewlayer", text="", icon='HIDE_OFF')
# row.operator("rt.sync_visibility_from_viewport", text="", icon='RESTRICT_VIEW_OFF')
# row.operator("rt.sync_visibility_from_render", text="", icon='RESTRICT_RENDER_OFF')
layout.separator()
if self.show_filter == 'ALL':
vl_collections = self.conflict_collections
elif self.show_filter == 'EXCLUDED':
vl_collections = self.excluded_collection
elif self.show_filter == 'NOT_EXCLUDED':
vl_collections = self.included_collection
col = layout.column()
for vlcol in vl_collections:
row = col.row(align=False)
row.label(text=vlcol.name)
# Viewlayer collection settings
row.prop(vlcol, "exclude", text="", emboss=False)
row.prop(vlcol, "hide_viewport", text="", emboss=False)
# Direct collection properties
row.prop(vlcol.collection, 'hide_viewport', text='', emboss=False)
row.prop(vlcol.collection, 'hide_render', text='', emboss=False)
def execute(self, context):
return {'FINISHED'}
# endregion
# region Modifier conflicts
class RT_OT_list_modifier_visibility(Operator):
bl_idname = "rt.list_modifier_visibility"
bl_label = "List Objects Modifiers Visibility Conflicts"
bl_description = "List Modifier visibility conflicts, when viewport and render have different values"
bl_options = {"REGISTER"}
def invoke(self, context, event):
self.ob_list = []
for o in context.scene.objects:
if not len(o.modifiers):
continue
mods = []
# TODO: add check for conflict a in viewport/render subdiv level (sursurf > multires)
for m in o.modifiers:
if m.show_viewport != m.show_render:
if not mods:
self.ob_list.append([o, mods, "OUTLINER_OB_" + o.type])
mods.append(m)
self.ob_list.sort(key=lambda x: x[2]) # regroup by objects type (this or x[0] for object name)
return context.window_manager.invoke_props_dialog(self, width=350)
def draw(self, context):
layout = self.layout
if not self.ob_list:
layout.label(text='No modifier visibility conflict found', icon='CHECKMARK')
return
col = layout.column(align=False)
for ct, o in enumerate(self.ob_list):
if ct > 0:
col.separator()
for i, m in enumerate(o[1]):
row = col.row()
if i == 0:
# show object name and icon for first item
row.label(text=o[0].name, icon=o[2]) # Label only
## Select object
# row.operator('rt.select_object_by_name', text=o[0].name, icon=o[2], emboss=False).object_name = o[0].name
else:
# Subsequent rows, show empty label
row.label(text=' ', icon='BLANK1')
# row.label(text=m.name, icon='MODIFIER_ON')
op = row.operator('rt.select_object_by_name', text=m.name, icon='MODIFIER_ON')
op.object_name = o[0].name
op.modifier_name = m.name
row.prop(m, 'show_viewport', text='', emboss=False) # invert_checkbox=True
row.prop(m, 'show_render', text='', emboss=False) # invert_checkbox=True
def execute(self, context):
return {'FINISHED'}
# endregion
# region Register
classes = (
RT_PG_object_visibility,
RT_OT_sync_visibility,
RT_OT_list_viewport_render_visibility, # Only viewport and render
RT_OT_list_object_visibility_conflicts,
RT_OT_list_collection_visibility_conflicts,
RT_OT_list_modifier_visibility,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
# endregion