""" 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 return the keys of data-blocks that use other data-blocks. They are titled as such that the first part of the function name is the type of the data being passed in and the second part of the function name is the users of that type. e.g. If you were searching for all of the places where an image is used in a material would be searching for the image_materials() function. """ import bpy from ..utils import compat def collection_all(collection_key): # returns a list of keys of every data-block that uses this collection return collection_cameras(collection_key) + \ collection_children(collection_key) + \ collection_lights(collection_key) + \ collection_meshes(collection_key) + \ collection_others(collection_key) + \ collection_rigidbody_world(collection_key) + \ collection_scenes(collection_key) + \ collection_instances(collection_key) def collection_cameras(collection_key): # recursively returns a list of camera object keys that are in the # collection and its child collections users = [] collection = bpy.data.collections[collection_key] # append all camera objects in our collection for obj in collection.objects: if obj.type == 'CAMERA': users.append(obj.name) # list of all child collections in our collection children = collection_children(collection_key) # append all camera objects from the child collections for child in children: for obj in bpy.data.collections[child].objects: if obj.type == 'CAMERA': users.append(obj.name) return distinct(users) def collection_children(collection_key): # returns a list of all child collections under the specified # collection using recursive functions collection = bpy.data.collections[collection_key] children = collection_children_recursive(collection_key) children.remove(collection.name) return children def collection_children_recursive(collection_key): # recursively returns a list of all child collections under the # specified collection including the collection itself collection = bpy.data.collections[collection_key] # base case if not collection.children: return [collection.name] # recursion case else: children = [] for child in collection.children: children += collection_children(child.name) children.append(collection.name) return children def collection_lights(collection_key): # returns a list of light object keys that are in the collection users = [] collection = bpy.data.collections[collection_key] # append all light objects in our collection for obj in collection.objects: if obj.type == 'LIGHT': users.append(obj.name) # list of all child collections in our collection children = collection_children(collection_key) # append all light objects from the child collections for child in children: for obj in bpy.data.collections[child].objects: if obj.type == 'LIGHT': users.append(obj.name) return distinct(users) def collection_meshes(collection_key): # returns a list of mesh object keys that are in the collection users = [] collection = bpy.data.collections[collection_key] # append all mesh objects in our collection and from child # collections for obj in collection.all_objects: if obj.type == 'MESH': users.append(obj.name) return distinct(users) def collection_others(collection_key): # returns a list of other object keys that are in the collection # NOTE: excludes cameras, lights, and meshes users = [] collection = bpy.data.collections[collection_key] # object types to exclude from this search excluded_types = ['CAMERA', 'LIGHT', 'MESH'] # append all other objects in our collection and from child # collections for obj in collection.all_objects: if obj.type not in excluded_types: users.append(obj.name) return distinct(users) def collection_rigidbody_world(collection_key): # returns a list containing "RigidBodyWorld" if the collection is used # by any scene's rigidbody_world.collection users = [] collection = bpy.data.collections[collection_key] # check all scenes for rigidbody_world usage for scene in bpy.data.scenes: # check if scene has rigidbody_world and if it uses our collection if hasattr(scene, 'rigidbody_world') and scene.rigidbody_world: if hasattr(scene.rigidbody_world, 'collection') and scene.rigidbody_world.collection: if scene.rigidbody_world.collection.name == collection.name: users.append("RigidBodyWorld") return distinct(users) def collection_scenes(collection_key): # returns a list of scene names that include this collection anywhere in # their collection hierarchy users = [] collection = bpy.data.collections[collection_key] for scene in bpy.data.scenes: if _scene_collection_contains(scene.collection, collection): users.append(scene.name) return distinct(users) def collection_instances(collection_key): # returns a list of object keys that instance this collection # Collection instances are objects with instance_type='COLLECTION' and # instance_collection pointing to this collection # Only counts objects that are actually in scene collections users = [] collection = bpy.data.collections[collection_key] for obj in bpy.data.objects: # Skip library-linked and override objects if compat.is_library_or_override(obj): continue # Check if object is a collection instance if hasattr(obj, 'instance_type') and obj.instance_type == 'COLLECTION': if hasattr(obj, 'instance_collection') and obj.instance_collection: if obj.instance_collection.name == collection.name: # Only count if the instance object is in a scene # (otherwise the collection isn't really being used) if object_all(obj.name): users.append(obj.name) return distinct(users) def _scene_collection_contains(parent_collection, target_collection): # helper that checks whether target_collection exists inside the # parent_collection hierarchy if parent_collection.name == target_collection.name: return True for child in parent_collection.children: if _scene_collection_contains(child, target_collection): return True return False def image_all(image_key): # returns a list of keys of every data-block that uses this image return image_compositors(image_key) + \ image_materials(image_key) + \ image_node_groups(image_key) + \ image_textures(image_key) + \ image_worlds(image_key) + \ image_geometry_nodes(image_key) def image_compositors(image_key): # returns a list containing "Compositor" if the image is used in # the scene's compositor users = [] image = bpy.data.images[image_key] # a list of node groups that use our image node_group_users = image_node_groups(image_key) # Import compat module for version-safe compositor access from ..utils import compat # if our compositor uses nodes and has a valid node tree scene = bpy.context.scene if scene.use_nodes: node_tree = compat.get_scene_compositor_node_tree(scene) if node_tree: # check each node in the compositor for node in node_tree.nodes: # if the node is an image node with a valid image if hasattr(node, 'image') and node.image: # if the node's image is our image if node.image.name == image.name: users.append("Compositor") # if the node is a group node with a valid node tree elif hasattr(node, 'node_tree') and node.node_tree: # if the node tree's name is in our list of node group # users if node.node_tree.name in node_group_users: users.append("Compositor") return distinct(users) def image_materials(image_key): # returns a list of material keys that use the image # Only returns materials that are actually used (in scenes) # This ensures images are correctly detected as unused when their # materials are unused (fixes issue #5) users = [] image = bpy.data.images[image_key] # list of node groups that use this image node_group_users = image_node_groups(image_key) for mat in bpy.data.materials: # Skip library-linked and override materials from ..utils import compat if compat.is_library_or_override(mat): continue # Check if this material uses the image material_uses_image = False # if material uses a valid node tree, check each node if mat.use_nodes and mat.node_tree: for node in mat.node_tree.nodes: # if node is has a not none image attribute if hasattr(node, 'image') and node.image: # if the nodes image is our image if node.image.name == image.name: material_uses_image = True # if image in node in node group in node tree elif node.type == 'GROUP': # if node group has a valid node tree and is in our # list of node groups that use this image if node.node_tree and \ node.node_tree.name in node_group_users: material_uses_image = True # Only add material if it uses the image AND is actually used if material_uses_image: # Check if material is actually used (in scenes) if material_all(mat.name): users.append(mat.name) return distinct(users) def image_node_groups(image_key): # returns a list of keys of node groups that use this image # Only returns node groups that are actually used (in scenes) # This ensures images are correctly detected as unused when their # node groups are unused (fixes issue #5) users = [] image = bpy.data.images[image_key] # for each node group for node_group in bpy.data.node_groups: # Skip library-linked and override node groups from ..utils import compat if compat.is_library_or_override(node_group): continue # if node group contains our image if node_group_has_image(node_group.name, image.name): # Only add node group if it is actually used (in scenes) if node_group_all(node_group.name): users.append(node_group.name) return distinct(users) def image_textures(image_key): # returns a list of texture keys that use the image if not hasattr(bpy.data, 'textures'): return [] users = [] image = bpy.data.images[image_key] # list of node groups that use this image node_group_users = image_node_groups(image_key) for texture in bpy.data.textures: # if texture uses a valid node tree, check each node if texture.use_nodes and texture.node_tree: for node in texture.node_tree.nodes: # check image nodes that use this image if hasattr(node, 'image') and node.image: if node.image.name == image.name: users.append(texture.name) # check for node groups that use this image elif hasattr(node, 'node_tree') and node.node_tree: # if node group is in our list of node groups that # use this image if node.node_tree.name in node_group_users: users.append(texture.name) # otherwise check the texture's image attribute else: # if texture uses an image if hasattr(texture, 'image') and texture.image: # if texture image is our image if texture.image.name == image.name: users.append(texture.name) return distinct(users) def image_geometry_nodes(image_key): # returns a list of object keys that use the image through Geometry Nodes # Only returns objects that are actually used (in scenes) # This ensures images are correctly detected as unused when their # objects are unused (fixes issue #5) users = [] image = bpy.data.images[image_key] # list of node groups that use this image node_group_users = image_node_groups(image_key) # Import compat module for version-safe geometry nodes access from ..utils import compat for obj in bpy.data.objects: # Skip library-linked and override objects if compat.is_library_or_override(obj): continue # Check if object is in any scene collection (reuse object_all logic) # This ensures recursive checking: if the object using the image isn't in a scene, # the image isn't considered used obj_scenes = object_all(obj.name) is_in_scene = bool(obj_scenes) if not is_in_scene: continue # Skip objects not in scene collections # check Geometry Nodes modifiers if hasattr(obj, 'modifiers'): for modifier in obj.modifiers: if compat.is_geometry_nodes_modifier(modifier): ng = compat.get_geometry_nodes_modifier_node_group(modifier) if ng: # direct usage in the modifier's tree if node_group_has_image(ng.name, image.name): users.append(obj.name) # usage via nested node groups elif ng.name in node_group_users: users.append(obj.name) return distinct(users) def image_worlds(image_key): # returns a list of world keys that use the image users = [] image = bpy.data.images[image_key] # list of node groups that use this image node_group_users = image_node_groups(image_key) for world in bpy.data.worlds: # if world uses a valid node tree, check each node if world.use_nodes and world.node_tree: for node in world.node_tree.nodes: # check image nodes if hasattr(node, 'image') and node.image: if node.image.name == image.name: users.append(world.name) # check for node groups that use this image elif hasattr(node, 'node_tree') and node.node_tree: if node.node_tree.name in node_group_users: users.append(world.name) return distinct(users) def light_all(light_key): # returns a list of keys of every data-block that uses this light return light_objects(light_key) def light_objects(light_key): # returns a list of light object keys that use the light data users = [] light = bpy.data.lights[light_key] for obj in bpy.data.objects: if obj.type == 'LIGHT' and obj.data: if obj.data.name == light.name: users.append(obj.name) return distinct(users) def material_all(material_key): # returns a list of keys of every data-block that uses this material # Use comprehensive custom detection that covers all usage contexts users = [] # Check direct object usage (material slots) users.extend(material_objects(material_key)) # Check Geometry Nodes usage (materials in node groups used by objects) users.extend(material_geometry_nodes(material_key)) # Check node group usage (materials in node groups used elsewhere) users.extend(material_node_groups(material_key)) # Check brush usage (materials used by brushes for stroke) users.extend(material_brushes(material_key)) return distinct(users) def material_brushes(material_key): # returns a list of brush keys that use this material # Brushes use materials for stroke rendering (Grease Pencil brushes) users = [] try: material = bpy.data.materials[material_key] except (KeyError, AttributeError): return [] if not hasattr(bpy.data, 'brushes'): return [] for brush in bpy.data.brushes: # Grease Pencil brushes use materials via gpencil_settings if hasattr(brush, 'gpencil_settings'): gp_settings = brush.gpencil_settings if gp_settings: # Check material property in gpencil_settings if hasattr(gp_settings, 'material'): gp_mat = gp_settings.material # Compare by datablock reference to avoid matching linked materials with same name if gp_mat and gp_mat == material: users.append(brush.name) # Check material_index - need to get material from Grease Pencil object if hasattr(gp_settings, 'material_index'): mat_idx = gp_settings.material_index # Check all Grease Pencil objects for this material for gp_obj in bpy.data.objects: if gp_obj.type == 'GPENCIL' and gp_obj.data: gp_data = gp_obj.data if hasattr(gp_data, 'materials') and gp_data.materials: if 0 <= mat_idx < len(gp_data.materials): gp_mat = gp_data.materials[mat_idx] # Compare by datablock reference to avoid matching linked materials with same name if gp_mat and gp_mat == material: users.append(brush.name) # Also check for stroke_material (some brush types) if hasattr(brush, 'stroke_material'): stroke_mat = brush.stroke_material # Compare by datablock reference to avoid matching linked materials with same name if stroke_mat and stroke_mat == material: users.append(brush.name) # Check for material property (some brush types) if hasattr(brush, 'material'): mat = brush.material # Compare by datablock reference to avoid matching linked materials with same name if mat and mat == material: users.append(brush.name) return distinct(users) def material_geometry_nodes(material_key): # returns a list of object keys that use the material via Geometry Nodes # Only counts objects that are in scene collections (recursive check) users = [] try: material = bpy.data.materials[material_key] except (KeyError, AttributeError): return [] # Import compat module for version-safe geometry nodes access from ..utils import compat for obj in bpy.data.objects: # Skip library-linked and override objects if compat.is_library_or_override(obj): continue # Check if object is in any scene collection (reuse object_all logic) # This ensures recursive checking: if the object using the material isn't in a scene, # the material isn't considered used obj_scenes = object_all(obj.name) is_in_scene = bool(obj_scenes) if not is_in_scene: continue # Skip objects not in scene collections if hasattr(obj, 'modifiers'): for modifier in obj.modifiers: if compat.is_geometry_nodes_modifier(modifier): ng = compat.get_geometry_nodes_modifier_node_group(modifier) if ng: # Check if this node group or any nested node groups contain the material # Pass material datablock reference to ensure we match the correct material if node_group_has_material_by_ref(ng.name, material): users.append(obj.name) return distinct(users) def material_node_groups(material_key): # returns a list of keys indicating where the material is used via node groups # This checks if the material is used in any node group, and if that node group # is itself used anywhere. This complements material_geometry_nodes() by checking # additional usage contexts (materials, other node groups, compositor, etc.) # Note: Geometry Nodes usage is already checked by material_geometry_nodes() # Optimized to return early when usage is found from ..utils import compat try: material = bpy.data.materials[material_key] except (KeyError, AttributeError): return [] # Check all node groups to see if they contain this material for node_group in bpy.data.node_groups: # Skip library-linked and override node groups if compat.is_library_or_override(node_group): continue # Use the by_ref version to avoid name collision issues with linked materials if node_group_has_material_by_ref(node_group.name, material): # This node group contains the material, check if the node group is used # Check usage contexts in order of likelihood, return early when found # First check: is it used in Geometry Nodes modifiers? (most common case) # Note: material_geometry_nodes() already checks this, but we verify here too obj_users = node_group_objects(node_group.name) if obj_users: return obj_users # Return immediately - material is used # Second check: is it used in materials? mat_users = node_group_materials(node_group.name) if mat_users: return mat_users # Return immediately - material is used # Third check: is it used in compositor? comp_users = node_group_compositors(node_group.name) if comp_users: return comp_users # Return immediately - material is used # Fourth check: is it used in textures? tex_users = node_group_textures(node_group.name) if tex_users: return tex_users # Return immediately - material is used # Fifth check: is it used in worlds? world_users = node_group_worlds(node_group.name) if world_users: return world_users # Return immediately - material is used # Last check: is it used in other node groups? (recursive, but only if needed) ng_users = node_group_node_groups(node_group.name) if ng_users: # Check if any parent node groups are used (quick check only) for parent_ng_name in ng_users: # Quick check: see if parent is used in objects (most common) parent_obj_users = node_group_objects(parent_ng_name) if parent_obj_users: return parent_obj_users # Quick check: see if parent is used in materials parent_mat_users = node_group_materials(parent_ng_name) if parent_mat_users: return parent_mat_users # Also check if parent is used in compositor, textures, worlds parent_comp_users = node_group_compositors(parent_ng_name) if parent_comp_users: return parent_comp_users parent_tex_users = node_group_textures(parent_ng_name) if parent_tex_users: return parent_tex_users parent_world_users = node_group_worlds(parent_ng_name) if parent_world_users: return parent_world_users return [] # Material not used in any node groups def material_node_groups_list(material_key): # returns a list of node group names that contain this material # This is used for inspection UI to show which node groups use the material # Unlike material_node_groups(), this returns all node groups that contain # the material, regardless of whether they're used from ..utils import compat users = [] try: material = bpy.data.materials[material_key] except (KeyError, AttributeError): return [] # Check all node groups to see if they contain this material for node_group in bpy.data.node_groups: # Skip library-linked and override node groups if compat.is_library_or_override(node_group): continue # Use the by_ref version to avoid name collision issues with linked materials if node_group_has_material_by_ref(node_group.name, material): users.append(node_group.name) return distinct(users) def material_objects(material_key): # returns a list of object keys that use this material users = [] try: material = bpy.data.materials[material_key] except (KeyError, AttributeError): return [] for obj in bpy.data.objects: # if the object has the option to add materials if hasattr(obj, 'material_slots'): # for each material slot for slot in obj.material_slots: # if material slot has a valid material, check if it's our material # Compare by datablock reference first to avoid matching linked materials with same name if slot.material: # Compare by reference (handles name collisions between local and linked materials) if slot.material == material: users.append(obj.name) return distinct(users) def node_group_all(node_group_key): # returns a list of keys of every data-block that uses this node group return node_group_compositors(node_group_key) + \ node_group_materials(node_group_key) + \ node_group_node_groups(node_group_key) + \ node_group_textures(node_group_key) + \ node_group_worlds(node_group_key) + \ node_group_objects(node_group_key) def node_group_compositors(node_group_key): # returns a list containing "Compositor" if the node group is used in # any scene's compositor (either as the compositor's node tree itself, or as a node within it) users = [] node_group = bpy.data.node_groups[node_group_key] # a list of node groups that use our node group node_group_users = node_group_node_groups(node_group_key) # Import compat and version modules for version-safe compositor access from ..utils import compat from ..utils import version # Check ALL scenes, not just the current one for scene in bpy.data.scenes: # First check: is this node group the compositor's node tree itself? if version.is_version_at_least(5, 0, 0): if hasattr(scene, 'compositing_node_tree') and scene.compositing_node_tree: # Check by reference, not just name (in case user renamed it) if scene.compositing_node_tree == node_group: users.append("Compositor") continue # Already found, skip checking nodes within it else: if hasattr(scene, 'node_tree') and scene.node_tree: # Check by reference, not just name (in case user renamed it) if scene.node_tree == node_group: users.append("Compositor") continue # Already found, skip checking nodes within it # Second check: is this node group used as a node within the compositor? if scene.use_nodes: node_tree = compat.get_scene_compositor_node_tree(scene) if node_tree: # check each node in the compositor for node in node_tree.nodes: # if the node is a group and has a valid node tree if hasattr(node, 'node_tree') and node.node_tree: # if the node group is our node group (check by reference) if node.node_tree == node_group: users.append("Compositor") # if the node group is in our list of node group users elif node.node_tree.name in node_group_users: users.append("Compositor") return distinct(users) def node_group_materials(node_group_key): # returns a list of material keys that use the node group in their # node trees # Note: Unlike image_materials(), this returns ALL materials using the node group, # not just used ones. This allows node_groups_deep() to check if materials are unused. users = [] node_group = bpy.data.node_groups[node_group_key] # node groups that use this node group node_group_users = node_group_node_groups(node_group_key) for material in bpy.data.materials: # Skip library-linked and override materials from ..utils import compat if compat.is_library_or_override(material): continue # if material uses nodes and has a valid node tree, check each node if material.use_nodes and material.node_tree: for node in material.node_tree.nodes: # if node is a group node if hasattr(node, 'node_tree') and node.node_tree: # if node is the node group if node.node_tree.name == node_group.name: users.append(material.name) # if node is using a node group contains our node group if node.node_tree.name in node_group_users: users.append(material.name) return distinct(users) def node_group_node_groups(node_group_key): # returns a list of all node groups that use this node group in # their node tree users = [] node_group = bpy.data.node_groups[node_group_key] # for each search group for search_group in bpy.data.node_groups: # if the search group contains our node group if node_group_has_node_group( search_group.name, node_group.name): users.append(search_group.name) return distinct(users) def node_group_textures(node_group_key): # returns a list of texture keys that use this node group in their # node trees if not hasattr(bpy.data, 'textures'): return [] users = [] node_group = bpy.data.node_groups[node_group_key] # list of node groups that use this node group node_group_users = node_group_node_groups(node_group_key) for texture in bpy.data.textures: # if texture uses a valid node tree, check each node if texture.use_nodes and texture.node_tree: for node in texture.node_tree.nodes: # check if node is a node group and has a valid node tree if hasattr(node, 'node_tree') and node.node_tree: # if node is our node group if node.node_tree.name == node_group.name: users.append(texture.name) # if node is a node group that contains our node group if node.node_tree.name in node_group_users: users.append(texture.name) return distinct(users) def node_group_worlds(node_group_key): # returns a list of world keys that use the node group in their node # trees users = [] node_group = bpy.data.node_groups[node_group_key] # node groups that use this node group node_group_users = node_group_node_groups(node_group_key) for world in bpy.data.worlds: # if world uses nodes and has a valid node tree if world.use_nodes and world.node_tree: for node in world.node_tree.nodes: # if node is a node group and has a valid node tree if hasattr(node, 'node_tree') and node.node_tree: # if this node is our node group if node.node_tree.name == node_group.name: users.append(world.name) # if this node is one of the node groups that use # our node group elif node.node_tree.name in node_group_users: users.append(world.name) return distinct(users) def node_group_objects(node_group_key): # returns a list of object keys that use this node group via Geometry Nodes modifiers users = [] node_group = bpy.data.node_groups[node_group_key] # node groups that use this node group node_group_users = node_group_node_groups(node_group_key) # Import compat module for version-safe geometry nodes access from ..utils import compat for obj in bpy.data.objects: if hasattr(obj, 'modifiers'): for modifier in obj.modifiers: if compat.is_geometry_nodes_modifier(modifier): ng = compat.get_geometry_nodes_modifier_node_group(modifier) if ng: if ng.name == node_group.name or ng.name in node_group_users: users.append(obj.name) return distinct(users) def _check_node_input_sockets_for_image(node, image_key): """Helper function to check if any input socket of a node contains the image. This handles nodes like Menu Switch that have materials/images in input sockets.""" try: image = bpy.data.images[image_key] if not hasattr(node, 'inputs'): return False for input_socket in node.inputs: try: # Check if socket has a default_value that is an image if hasattr(input_socket, 'default_value') and input_socket.default_value: socket_value = input_socket.default_value # Check if it's an image datablock if hasattr(socket_value, 'name') and hasattr(socket_value, 'filepath'): if socket_value.name == image.name or socket_value == image: return True except (AttributeError, ReferenceError, RuntimeError, TypeError, KeyError): continue # Skip this socket if we can't access it except (KeyError, AttributeError): return False return False def node_group_has_image(node_group_key, image_key): # recursively returns true if the node group contains this image # directly or if it contains a node group a node group that contains # the image indirectly has_image = False node_group = bpy.data.node_groups[node_group_key] image = bpy.data.images[image_key] # for each node in our search group for node in node_group.nodes: # base case # if node has a not none image attribute if hasattr(node, 'image') and node.image: # if the node group is our node group if node.image.name == image.name: has_image = True # Check input sockets for images (e.g., Menu Switch nodes) # This handles nodes that have images connected via input sockets if not has_image: has_image = _check_node_input_sockets_for_image(node, image_key) # recurse case # if node is a node group and has a valid node tree if not has_image and hasattr(node, 'node_tree') and node.node_tree: has_image = node_group_has_image( node.node_tree.name, image.name) # break the loop if the image is found if has_image: break return has_image def node_group_has_node_group(search_group_key, node_group_key): # returns true if a node group contains this node group has_node_group = False search_group = bpy.data.node_groups[search_group_key] node_group = bpy.data.node_groups[node_group_key] # for each node in our search group for node in search_group.nodes: # if node is a node group and has a valid node tree if hasattr(node, 'node_tree') and node.node_tree: if node.node_tree.name == "RG_MetallicMap": print(node.node_tree.name) print(node_group.name) # base case # if node group is our node group if node.node_tree.name == node_group.name: has_node_group = True # recurse case # if node group is any other node group else: has_node_group = node_group_has_node_group( node.node_tree.name, node_group.name) # break the loop if the node group is found if has_node_group: break return has_node_group def node_group_has_texture(node_group_key, texture_key): # returns true if a node group contains this image has_texture = False if not hasattr(bpy.data, 'textures'): return has_texture node_group = bpy.data.node_groups[node_group_key] texture = bpy.data.textures[texture_key] # for each node in our search group for node in node_group.nodes: # base case # if node has a not none image attribute if hasattr(node, 'texture') and node.texture: # if the node group is our node group if node.texture.name == texture.name: has_texture = True # recurse case # if node is a node group and has a valid node tree elif hasattr(node, 'node_tree') and node.node_tree: has_texture = node_group_has_texture( node.node_tree.name, texture.name) # break the loop if the texture is found if has_texture: break return has_texture def _check_node_input_sockets_for_material(node, material_key): """Helper function to check if any input socket of a node contains the material. This handles nodes like Menu Switch that have materials in input sockets.""" try: material = bpy.data.materials[material_key] return _check_node_input_sockets_for_material_by_ref(node, material) except (KeyError, AttributeError): return False def _check_node_input_sockets_for_material_by_ref(node, material): """Helper function to check if any input socket of a node contains the material. Takes material datablock directly to avoid name collision issues with linked materials.""" if not material or not hasattr(node, 'inputs'): return False for input_socket in node.inputs: try: # Check socket type - material sockets are typically 'MATERIAL' type socket_type = getattr(input_socket, 'type', '') if socket_type == 'MATERIAL' or 'material' in str(socket_type).lower(): # Check if this socket has a default_value that is a material if hasattr(input_socket, 'default_value') and input_socket.default_value: socket_material = input_socket.default_value # Compare by datablock reference to avoid matching linked materials with same name if socket_material and socket_material == material: return True except (AttributeError, ReferenceError, RuntimeError, TypeError, KeyError): continue # Skip this socket if we can't access it return False def node_group_has_material_by_ref(node_group_key, material): # returns true if a node group contains this material (directly or nested) # Takes material datablock directly to avoid name collision issues with linked materials has_material = False try: node_group = bpy.data.node_groups[node_group_key] except (KeyError, AttributeError): return False if not material: return False try: for node in node_group.nodes: try: # Explicitly check for GeometryNodeSetMaterial nodes first # This is the most reliable way to detect Set Material nodes in Geometry Nodes if hasattr(node, 'bl_idname'): try: if node.bl_idname == 'GeometryNodeSetMaterial': # Geometry Nodes Set Material nodes use input sockets, not a direct material property # Check the material input socket try: # Try to access the Material input socket directly by name if hasattr(node, 'inputs') and 'Material' in node.inputs: try: material_socket = node.inputs['Material'] # Check the default_value (for unlinked materials) if hasattr(material_socket, 'default_value'): socket_material = material_socket.default_value # Compare by datablock reference to avoid matching linked materials with same name if socket_material and socket_material == material: has_material = True except (KeyError, AttributeError, ReferenceError, RuntimeError, TypeError): pass # Also check all inputs as fallback (in case socket name differs) if not has_material: for input_socket in getattr(node, 'inputs', []): try: # Check socket type - material sockets are typically 'MATERIAL' type socket_type = getattr(input_socket, 'type', '') if socket_type == 'MATERIAL' or 'material' in str(input_socket).lower(): # Check if this socket has a default_value that is a material if hasattr(input_socket, 'default_value') and input_socket.default_value: socket_material = input_socket.default_value # Compare by datablock reference to avoid matching linked materials with same name if socket_material and socket_material == material: has_material = True break # Found it, no need to continue except (AttributeError, ReferenceError, RuntimeError, TypeError): continue # Skip this socket if we can't access it # Also check if the node has a direct material property (fallback for some versions) if not has_material and hasattr(node, 'material'): try: if node.material: # Compare by datablock reference to avoid matching linked materials with same name if node.material == material: has_material = True break # Found it, no need to continue except (AttributeError, ReferenceError, RuntimeError): pass # Skip if material access fails if has_material: break # Break outer loop if we found it except (AttributeError, ReferenceError, RuntimeError): pass # Skip if Set Material node input access fails except (AttributeError, ReferenceError, RuntimeError): pass # Skip if bl_idname access fails # Check for Menu Switch nodes and other nodes with material inputs # Menu Switch nodes can have materials in any of their input sockets if not has_material and hasattr(node, 'bl_idname'): try: node_type = node.bl_idname # Check for Menu Switch node (GeometryNodeMenuSwitch) if node_type == 'GeometryNodeMenuSwitch' or 'MenuSwitch' in node_type: has_material = _check_node_input_sockets_for_material_by_ref(node, material) if has_material: break except (AttributeError, ReferenceError, RuntimeError): pass # Skip if bl_idname access fails # General check: Check all input sockets for materials (catches other node types) # This is a fallback for any node that might have material inputs if not has_material: has_material = _check_node_input_sockets_for_material_by_ref(node, material) if has_material: break # Fallback: Check for any node with a material property (e.g., Set Material) # This catches other node types that might have materials if not has_material and hasattr(node, 'material'): try: if node.material: # Compare by datablock reference to avoid matching linked materials with same name if node.material == material: has_material = True break # Found it, no need to continue except (AttributeError, ReferenceError, RuntimeError): pass # Skip if material access fails # Also check node type by substring for Set Material nodes (backup check) if not has_material and hasattr(node, 'bl_idname'): try: node_type = node.bl_idname # Check for Geometry Nodes Set Material node type (substring match) if 'SetMaterial' in node_type or 'SET_MATERIAL' in node_type.upper(): if hasattr(node, 'material'): try: if node.material: # Compare by datablock reference to avoid matching linked materials with same name if node.material == material: has_material = True break except (AttributeError, ReferenceError, RuntimeError): pass # Skip if material access fails except (AttributeError, ReferenceError, RuntimeError): pass # Skip if bl_idname access fails # recurse case: nested node groups # Check this separately (not elif) in case we need to recurse if not has_material and hasattr(node, 'node_tree'): try: if node.node_tree: has_material = node_group_has_material_by_ref( node.node_tree.name, material) except (KeyError, AttributeError, ReferenceError, RuntimeError): continue # Skip invalid node groups if has_material: break except (AttributeError, ReferenceError, RuntimeError): # Skip nodes that cause errors (e.g., invalid/corrupted nodes) continue except (AttributeError, ReferenceError, RuntimeError): # If we can't even iterate nodes, return False return False return has_material def node_group_has_material(node_group_key, material_key): # returns true if a node group contains this material (directly or nested) # Wrapper that converts material_key to material datablock for reference-based comparison try: material = bpy.data.materials[material_key] except (KeyError, AttributeError): return False return node_group_has_material_by_ref(node_group_key, material) def particle_all(particle_key): # returns a list of keys of every data-block that uses this particle # system return particle_objects(particle_key) def particle_objects(particle_key): # returns a list of object keys that use the particle system if not hasattr(bpy.data, 'particles'): return [] users = [] particle_system = bpy.data.particles[particle_key] for obj in bpy.data.objects: # if object can have a particle system if hasattr(obj, 'particle_systems'): for particle in obj.particle_systems: # if particle settings is our particle system if particle.settings.name == particle_system.name: users.append(obj.name) return distinct(users) def texture_all(texture_key): # returns a list of keys of every data-block that uses this texture return texture_brushes(texture_key) + \ texture_compositor(texture_key) + \ texture_objects(texture_key) + \ texture_node_groups(texture_key) + \ texture_particles(texture_key) def texture_brushes(texture_key): # returns a list of brush keys that use the texture if not hasattr(bpy.data, 'textures'): return [] users = [] texture = bpy.data.textures[texture_key] for brush in bpy.data.brushes: # if brush has a texture if brush.texture: # if brush texture is our texture if brush.texture.name == texture.name: users.append(brush.name) return distinct(users) def texture_compositor(texture_key): # returns a list containing "Compositor" if the texture is used in # the scene's compositor if not hasattr(bpy.data, 'textures'): return [] users = [] texture = bpy.data.textures[texture_key] # a list of node groups that use our image node_group_users = texture_node_groups(texture_key) # Import compat module for version-safe compositor access from ..utils import compat # if our compositor uses nodes and has a valid node tree scene = bpy.context.scene if scene.use_nodes: node_tree = compat.get_scene_compositor_node_tree(scene) if node_tree: # check each node in the compositor for node in node_tree.nodes: # if the node is an texture node with a valid texture if hasattr(node, 'texture') and node.texture: # if the node's texture is our texture if node.texture.name == texture.name: users.append("Compositor") # if the node is a group node with a valid node tree elif hasattr(node, 'node_tree') and node.node_tree: # if the node tree's name is in our list of node group # users if node.node_tree.name in node_group_users: users.append("Compositor") return distinct(users) def texture_objects(texture_key): # returns a list of object keys that use the texture in one of their # modifiers if not hasattr(bpy.data, 'textures'): return [] users = [] texture = bpy.data.textures[texture_key] # list of particle systems that use our texture particle_users = texture_particles(texture_key) # append objects that use the texture in a modifier for obj in bpy.data.objects: # if object can have modifiers applied to it if hasattr(obj, 'modifiers'): for modifier in obj.modifiers: # if the modifier has a texture attribute that is not None if hasattr(modifier, 'texture') \ and modifier.texture: if modifier.texture.name == texture.name: users.append(obj.name) # if the modifier has a mask_texture attribute that is # not None elif hasattr(modifier, 'mask_texture') \ and modifier.mask_texture: if modifier.mask_texture.name == texture.name: users.append(obj.name) # append objects that use the texture in a particle system for particle in particle_users: # append all objects that use the particle system users += particle_objects(particle) return distinct(users) def texture_node_groups(texture_key): # returns a list of keys of all node groups that use this texture if not hasattr(bpy.data, 'textures'): return [] users = [] texture = bpy.data.textures[texture_key] # for each node group for node_group in bpy.data.node_groups: # if node group contains our texture if node_group_has_texture( node_group.name, texture.name): users.append(node_group.name) return distinct(users) def texture_particles(texture_key): # returns a list of particle system keys that use the texture in # their texture slots if not hasattr(bpy.data, 'textures') or not hasattr(bpy.data, 'particles'): return [] users = [] texture = bpy.data.textures[texture_key] for particle in bpy.data.particles: # for each texture slot in the particle system for texture_slot in particle.texture_slots: # if texture slot has a texture that is not None if hasattr(texture_slot, 'texture') and texture_slot.texture: # if texture in texture slot is our texture if texture_slot.texture.name == texture.name: users.append(particle.name) return distinct(users) def object_all(object_key, _visited_objects=None): # returns a list of scene names where the object is used # An object is "used" if it's in any collection that's part of any scene's collection hierarchy # OR if it's in a collection that is instanced (and that instance is in a scene) # _visited_objects is used to prevent infinite recursion when checking instanced collections if _visited_objects is None: _visited_objects = set() # Prevent infinite recursion if object_key in _visited_objects: return [] _visited_objects.add(object_key) try: users = [] obj = bpy.data.objects[object_key] # Get all collections that contain this object for collection in obj.users_collection: # Check if this collection is in any scene's hierarchy for scene in bpy.data.scenes: if _scene_collection_contains(scene.collection, collection): if scene.name not in users: users.append(scene.name) # Also check if this collection is instanced (and the instance is in a scene) # Get all objects that instance this collection for instance_obj in bpy.data.objects: # Skip library-linked and override objects if compat.is_library_or_override(instance_obj): continue # Check if object is a collection instance if hasattr(instance_obj, 'instance_type') and instance_obj.instance_type == 'COLLECTION': if hasattr(instance_obj, 'instance_collection') and instance_obj.instance_collection: if instance_obj.instance_collection.name == collection.name: # Check if the instance object is in a scene (using visited set to prevent recursion) # First check if instance object is directly in a scene collection instance_direct_scenes = [] for instance_collection in instance_obj.users_collection: for scene in bpy.data.scenes: if _scene_collection_contains(scene.collection, instance_collection): if scene.name not in instance_direct_scenes: instance_direct_scenes.append(scene.name) # If instance object is directly in a scene, the instanced collection's objects are used if instance_direct_scenes: for scene_name in instance_direct_scenes: if scene_name not in users: users.append(scene_name) else: # Instance object is not directly in a scene, but might be in an instanced collection # Recursively check (with visited set to prevent infinite loops) instance_scenes = object_all(instance_obj.name, _visited_objects) for scene_name in instance_scenes: if scene_name not in users: users.append(scene_name) return distinct(users) finally: _visited_objects.remove(object_key) def armature_all(armature_key): # returns a list of object names that use the armature # Checks direct usage, modifier usage, and constraint usage # Only counts objects that are actually in scene collections (recursive check) users = [] armature = bpy.data.armatures[armature_key] # Check all objects - but only count those that are in scene collections for obj in bpy.data.objects: # Skip library-linked and override objects from ..utils import compat if compat.is_library_or_override(obj): continue # Check if object is in any scene collection (reuse object_all logic) obj_scenes = object_all(obj.name) is_in_scene = bool(obj_scenes) # Check for usage regardless of scene status (we'll filter later) found_usage = False # 1. Direct usage: ARMATURE objects where object.data == armature if obj.type == 'ARMATURE' and obj.data == armature: found_usage = True # 2. Modifier usage: Armature modifiers where modifier.object.data == armature if not found_usage and hasattr(obj, 'modifiers'): for modifier in obj.modifiers: if modifier.type == 'ARMATURE': if hasattr(modifier, 'object') and modifier.object: if modifier.object.type == 'ARMATURE' and modifier.object.data == armature: found_usage = True break # 3. Constraint usage: Constraints that target ARMATURE objects using this armature if not found_usage and hasattr(obj, 'constraints'): for constraint in obj.constraints: if hasattr(constraint, 'target') and constraint.target: if constraint.target.type == 'ARMATURE' and constraint.target.data == armature: found_usage = True break # Only add to users if the object is actually in a scene # This implements recursive checking: if the user object is unused, it doesn't count if found_usage and is_in_scene: users.append(obj.name) return distinct(users) def distinct(seq): # returns a list of distinct elements return list(set(seq))