""" Copyright (C) 2019 Remington Creative This file is part of Atomic Data Manager. Atomic Data Manager is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Atomic Data Manager is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with Atomic Data Manager. If not, see . --- This file contains functions that detect data-blocks that have no users, as determined by stats.users.py """ import bpy from .. import config from ..utils import compat from ..utils import version from . import users def shallow(data): # returns a list of keys of unused data-blocks in the data that may be # incomplete, but is significantly faster than doing a deep search unused = [] for datablock in data: # Skip library-linked and override datablocks if compat.is_library_or_override(datablock): continue # if data-block has no users or if it has a fake user and # ignore fake users is enabled if datablock.users == 0 or (datablock.users == 1 and datablock.use_fake_user and config.include_fake_users): unused.append(datablock.name) return unused def collections_deep(): # returns a full list of keys of unused collections unused = [] for collection in bpy.data.collections: # Skip library-linked and override datablocks if compat.is_library_or_override(collection): continue if not users.collection_all(collection.name): unused.append(collection.name) return unused def collections_shallow(): # returns a list of keys of unused collections that may be # incomplete, but is significantly faster. unused = [] for collection in bpy.data.collections: # Skip library-linked and override datablocks if compat.is_library_or_override(collection): continue if not (collection.objects or collection.children): unused.append(collection.name) return unused def images_deep(): # returns a full list of keys of unused images unused = [] # a list of image keys that should not be flagged as unused # this list also exists in images_shallow() do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"] total_images = len(bpy.data.images) config.debug_print(f"[Atomic Debug] images_deep(): Starting, total images: {total_images}") checked = 0 for image in bpy.data.images: # Skip library-linked and override datablocks if compat.is_library_or_override(image): continue checked += 1 config.debug_print(f"[Atomic Debug] images_deep(): Checking image {checked}/{total_images}: '{image.name}'") # First check: standard unused detection config.debug_print(f"[Atomic Debug] images_deep(): Calling users.image_all('{image.name}')...") if not users.image_all(image.name): config.debug_print(f"[Atomic Debug] images_deep(): Image '{image.name}' is unused (first check)") # check if image has a fake user or if ignore fake users # is enabled if not image.use_fake_user or config.include_fake_users: # if image is not in our do not flag list if image.name not in do_not_flag: unused.append(image.name) else: # Second check: image is used, but check if it's ONLY used by unused objects # This fixes issue #5: images used by unused objects should be marked as unused # Get all objects that use this image (directly or indirectly) objects_using_image = [] # Check materials that use the image config.debug_print(f"[Atomic Debug] images_deep(): Getting materials for '{image.name}'...") mat_names = users.image_materials(image.name) config.debug_print(f"[Atomic Debug] images_deep(): Found {len(mat_names)} materials using '{image.name}'") for mat_name in mat_names: # Get objects using this material config.debug_print(f"[Atomic Debug] images_deep(): Getting objects for material '{mat_name}'...") objects_using_image.extend(users.material_objects(mat_name)) # Also check Geometry Nodes usage config.debug_print(f"[Atomic Debug] images_deep(): Getting Geometry Nodes objects for material '{mat_name}'...") objects_using_image.extend(users.material_geometry_nodes(mat_name)) # Check Geometry Nodes directly config.debug_print(f"[Atomic Debug] images_deep(): Getting Geometry Nodes objects for '{image.name}'...") objects_using_image.extend(users.image_geometry_nodes(image.name)) # Remove duplicates objects_using_image = list(set(objects_using_image)) config.debug_print(f"[Atomic Debug] images_deep(): Found {len(objects_using_image)} objects using '{image.name}'") # If image is only used by objects, and ALL those objects are unused, mark image as unused # Check each object individually to avoid recursion issues if objects_using_image: config.debug_print(f"[Atomic Debug] images_deep(): Checking if all {len(objects_using_image)} objects are unused...") all_objects_unused = all(not users.object_all(obj_name) for obj_name in objects_using_image) if all_objects_unused: config.debug_print(f"[Atomic Debug] images_deep(): All objects are unused, marking '{image.name}' as unused") # Check if image has a fake user or if ignore fake users is enabled if not image.use_fake_user or config.include_fake_users: # if image is not in our do not flag list if image.name not in do_not_flag: unused.append(image.name) config.debug_print(f"[Atomic Debug] images_deep(): Added '{image.name}' to unused list") else: config.debug_print(f"[Atomic Debug] images_deep(): Some objects are used, '{image.name}' is not unused") config.debug_print(f"[Atomic Debug] images_deep(): Finished checking '{image.name}'") config.debug_print(f"[Atomic Debug] images_deep(): Complete, checked {checked} images, found {len(unused)} unused") return unused def images_shallow(): # returns a list of keys of unused images that may be # incomplete, but is significantly faster than doing a deep search unused_images = shallow(bpy.data.images) # a list of image keys that should not be flagged as unused # this list also exists in images_deep() do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"] # remove do not flag keys from unused images for key in do_not_flag: if key in unused_images: unused_images.remove(key) return unused_images def lights_deep(): # returns a list of keys of unused lights unused = [] for light in bpy.data.lights: # Skip library-linked and override datablocks if compat.is_library_or_override(light): continue if not users.light_all(light.name): # check if light has a fake user or if ignore fake users # is enabled if not light.use_fake_user or config.include_fake_users: unused.append(light.name) return unused def lights_shallow(): # returns a list of keys of unused lights that may be # incomplete, but is significantly faster than doing a deep search return shallow(bpy.data.lights) def materials_deep(): # returns a list of keys of unused materials unused = [] for material in bpy.data.materials: # Skip library-linked and override datablocks if compat.is_library_or_override(material): continue # Check if material is used by brushes - these should always be ignored if users.material_brushes(material.name): continue # First check: standard unused detection if not users.material_all(material.name): # check if material has a fake user or if ignore fake users # is enabled if not material.use_fake_user or config.include_fake_users: unused.append(material.name) else: # Second check: material is used, but check if it's ONLY used by unused objects # This fixes issue #5: materials used by unused objects should be marked as unused # Get all objects that use this material objects_using_material = [] objects_using_material.extend(users.material_objects(material.name)) objects_using_material.extend(users.material_geometry_nodes(material.name)) # Remove duplicates objects_using_material = list(set(objects_using_material)) # If material is only used by objects, and ALL those objects are unused, mark material as unused # Check each object individually to avoid recursion issues if objects_using_material: all_objects_unused = all(not users.object_all(obj_name) for obj_name in objects_using_material) if all_objects_unused: # Check if material has a fake user or if ignore fake users is enabled if not material.use_fake_user or config.include_fake_users: unused.append(material.name) return unused def materials_shallow(): # returns a list of keys of unused material that may be # incomplete, but is significantly faster than doing a deep search unused_materials = shallow(bpy.data.materials) # Filter out materials used by brushes - these should always be ignored filtered = [] for key in unused_materials: material = bpy.data.materials.get(key) if material and not users.material_brushes(key): filtered.append(key) return filtered def _is_compositor_node_tree(node_group): """ Check if a node group is a compositor node tree. In Blender 5.0+, each scene has a compositing_node_tree that should be ignored. Args: node_group: The node group to check Returns: bool: True if the node group is a compositor node tree """ # Check if this node group is any scene's compositor node tree # Use compat function to handle version differences properly for scene in bpy.data.scenes: if scene.use_nodes: node_tree = compat.get_scene_compositor_node_tree(scene) config.debug_print(f"[Atomic Debug] _is_compositor_node_tree: scene='{scene.name}', use_nodes={scene.use_nodes}, node_tree={node_tree}") if node_tree: config.debug_print(f"[Atomic Debug] _is_compositor_node_tree: node_tree.name='{node_tree.name}', checking against '{node_group.name}'") # Check by reference, not just name (in case user renamed it) if node_tree == node_group: config.debug_print(f"[Atomic Debug] _is_compositor_node_tree: '{node_group.name}' is scene '{scene.name}' compositor node tree") return True else: config.debug_print(f"[Atomic Debug] _is_compositor_node_tree: node_tree != node_group (reference comparison failed)") # Also check if it's used as a node within any compositor (via node_group_compositors) # This handles the case where the node group is used within a compositor, not just as the tree itself comp_users = users.node_group_compositors(node_group.name) if comp_users: config.debug_print(f"[Atomic Debug] _is_compositor_node_tree: '{node_group.name}' is used in compositor: {comp_users}") return True config.debug_print(f"[Atomic Debug] _is_compositor_node_tree: '{node_group.name}' is NOT a compositor node tree") return False def node_groups_deep(): # returns a list of keys of unused node_groups unused = [] # Track which node groups we've already determined are unused (to avoid infinite recursion) _unused_node_groups_cache = set() def _is_node_group_unused(ng_name, visited=None): """Recursively check if a node group is unused. Returns True if the node group is only used by unused materials/objects/node_groups.""" if visited is None: visited = set() # Avoid infinite recursion if ng_name in visited: return False visited.add(ng_name) # Check cache first if ng_name in _unused_node_groups_cache: return True node_group = bpy.data.node_groups.get(ng_name) if not node_group: return False # Skip library-linked and override datablocks if compat.is_library_or_override(node_group): return False # Skip compositor node trees if _is_compositor_node_tree(node_group): return False # First check: node group has no users at all all_users = users.node_group_all(ng_name) config.debug_print(f"[Atomic Debug] _is_node_group_unused: '{ng_name}' - all_users = {all_users}") if not all_users: if not node_group.use_fake_user or config.include_fake_users: config.debug_print(f"[Atomic Debug] _is_node_group_unused: '{ng_name}' has no users, marking as unused") _unused_node_groups_cache.add(ng_name) return True # Second check: node group is used, but check if it's ONLY used by unused materials/objects/node_groups # Get all materials and objects that use this node group materials_using_ng = users.node_group_materials(ng_name) objects_using_ng = users.node_group_objects(ng_name) parent_node_groups = users.node_group_node_groups(ng_name) # Collect all objects that use this node group (directly or via materials) all_objects_using_ng = list(objects_using_ng) # Direct object usage via geometry nodes # For each material using this node group, get objects using that material for mat_name in materials_using_ng: # Get objects using this material objects_using_mat = users.material_objects(mat_name) objects_using_mat.extend(users.material_geometry_nodes(mat_name)) all_objects_using_ng.extend(objects_using_mat) # Remove duplicates all_objects_using_ng = list(set(all_objects_using_ng)) # Check if all objects are unused all_objects_unused = True if all_objects_using_ng: all_objects_unused = all(not users.object_all(obj_name) for obj_name in all_objects_using_ng) # Check if all parent node groups are unused (recursive) all_parent_ngs_unused = True if parent_node_groups: for parent_ng_name in parent_node_groups: if not _is_node_group_unused(parent_ng_name, visited.copy()): all_parent_ngs_unused = False break # If node group is only used by unused objects and unused parent node groups, mark it as unused if all_objects_unused and all_parent_ngs_unused: if not node_group.use_fake_user or config.include_fake_users: _unused_node_groups_cache.add(ng_name) return True return False for node_group in bpy.data.node_groups: # Skip library-linked and override datablocks if compat.is_library_or_override(node_group): continue # Skip compositor node trees (Blender 5.0+ creates one per file) if _is_compositor_node_tree(node_group): continue if _is_node_group_unused(node_group.name): unused.append(node_group.name) return unused def node_groups_shallow(): # returns a list of keys of unused node groups that may be # incomplete, but is significantly faster than doing a deep search unused = shallow(bpy.data.node_groups) # Filter out compositor node trees (Blender 5.0+ creates one per file) filtered = [] for node_group_name in unused: node_group = bpy.data.node_groups.get(node_group_name) if node_group and not _is_compositor_node_tree(node_group): filtered.append(node_group_name) return filtered def particles_deep(): # returns a list of keys of unused particle systems if not hasattr(bpy.data, 'particles'): return [] unused = [] for particle in bpy.data.particles: # Skip library-linked and override datablocks if compat.is_library_or_override(particle): continue if not users.particle_all(particle.name): # check if particle system has a fake user or if ignore fake # users is enabled if not particle.use_fake_user or config.include_fake_users: unused.append(particle.name) return unused def particles_shallow(): # returns a list of keys of unused particle systems that may be # incomplete, but is significantly faster than doing a deep search return shallow(bpy.data.particles) if hasattr(bpy.data, 'particles') else [] def textures_deep(): # returns a list of keys of unused textures if not hasattr(bpy.data, 'textures'): return [] unused = [] for texture in bpy.data.textures: # Skip library-linked and override datablocks if compat.is_library_or_override(texture): continue if not users.texture_all(texture.name): # check if texture has a fake user or if ignore fake users # is enabled if not texture.use_fake_user or config.include_fake_users: unused.append(texture.name) return unused def textures_shallow(): # returns a list of keys of unused textures that may be # incomplete, but is significantly faster than doing a deep search return shallow(bpy.data.textures) if hasattr(bpy.data, 'textures') else [] def worlds(): # returns a full list of keys of unused worlds config.debug_print(f"[Atomic Debug] unused.worlds(): Starting, total worlds: {len(bpy.data.worlds)}") unused = [] checked = 0 for world in bpy.data.worlds: # Skip library-linked and override datablocks if compat.is_library_or_override(world): continue checked += 1 config.debug_print(f"[Atomic Debug] unused.worlds(): Checking '{world.name}' (users={world.users}, fake_user={world.use_fake_user})") # if data-block has no users or if it has a fake user and # ignore fake users is enabled if world.users == 0 or (world.users == 1 and world.use_fake_user and config.include_fake_users): config.debug_print(f"[Atomic Debug] unused.worlds(): '{world.name}' is unused, adding to list") unused.append(world.name) config.debug_print(f"[Atomic Debug] unused.worlds(): Complete, checked {checked} worlds, found {len(unused)} unused") return unused def objects_deep(): # returns a list of keys of unused objects unused = [] for obj in bpy.data.objects: # Skip library-linked and override datablocks if compat.is_library_or_override(obj): continue if not users.object_all(obj.name): # check if object has a fake user or if ignore fake users # is enabled if not obj.use_fake_user or config.include_fake_users: unused.append(obj.name) return unused def objects_shallow(): # returns a list of keys of unused objects that may be # incomplete, but is significantly faster than doing a deep search return shallow(bpy.data.objects) def armatures_deep(): # returns a list of keys of unused armatures unused = [] for armature in bpy.data.armatures: # Skip library-linked and override datablocks if compat.is_library_or_override(armature): continue if not users.armature_all(armature.name): # check if armature has a fake user or if ignore fake users # is enabled if not armature.use_fake_user or config.include_fake_users: unused.append(armature.name) return unused def armatures_shallow(): # returns a list of keys of unused armatures that may be # incomplete, but is significantly faster than doing a deep search return shallow(bpy.data.armatures)