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 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 vcols = get_collection_children_recursive(context.view_layer.layer_collection) vcols = list(set(vcols)) # ensure no duplicates ## Store collection with conflicts # layer_collection.is_visible against render visibility ? ## Do not list currently excluded collections 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