2026-01-01
This commit is contained in:
@@ -23,6 +23,7 @@ This file contains functions that count quantities of various sets of data.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from ..utils import compat
|
||||
from . import unused
|
||||
from . import unnamed
|
||||
from . import missing
|
||||
@@ -30,8 +31,11 @@ from . import missing
|
||||
|
||||
def collections():
|
||||
# returns the number of collections in the project
|
||||
|
||||
return len(bpy.data.collections)
|
||||
count = 0
|
||||
for collection in bpy.data.collections:
|
||||
if not compat.is_library_or_override(collection):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def collections_unused():
|
||||
@@ -48,8 +52,11 @@ def collections_unnamed():
|
||||
|
||||
def images():
|
||||
# returns the number of images in the project
|
||||
|
||||
return len(bpy.data.images)
|
||||
count = 0
|
||||
for image in bpy.data.images:
|
||||
if not compat.is_library_or_override(image):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def images_unused():
|
||||
@@ -72,8 +79,11 @@ def images_missing():
|
||||
|
||||
def lights():
|
||||
# returns the number of lights in the project
|
||||
|
||||
return len(bpy.data.lights)
|
||||
count = 0
|
||||
for light in bpy.data.lights:
|
||||
if not compat.is_library_or_override(light):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def lights_unused():
|
||||
@@ -90,8 +100,11 @@ def lights_unnamed():
|
||||
|
||||
def materials():
|
||||
# returns the number of materials in the project
|
||||
|
||||
return len(bpy.data.materials)
|
||||
count = 0
|
||||
for material in bpy.data.materials:
|
||||
if not compat.is_library_or_override(material):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def materials_unused():
|
||||
@@ -108,8 +121,11 @@ def materials_unnamed():
|
||||
|
||||
def node_groups():
|
||||
# returns the number of node groups in the project
|
||||
|
||||
return len(bpy.data.node_groups)
|
||||
count = 0
|
||||
for node_group in bpy.data.node_groups:
|
||||
if not compat.is_library_or_override(node_group):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def node_groups_unused():
|
||||
@@ -126,8 +142,11 @@ def node_groups_unnamed():
|
||||
|
||||
def objects():
|
||||
# returns the number of objects in the project
|
||||
|
||||
return len(bpy.data.objects)
|
||||
count = 0
|
||||
for obj in bpy.data.objects:
|
||||
if not compat.is_library_or_override(obj):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def objects_unnamed():
|
||||
@@ -138,8 +157,11 @@ def objects_unnamed():
|
||||
|
||||
def particles():
|
||||
# returns the number of particles in the project
|
||||
|
||||
return len(bpy.data.particles)
|
||||
count = 0
|
||||
for particle in bpy.data.particles:
|
||||
if not compat.is_library_or_override(particle):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def particles_unused():
|
||||
@@ -156,8 +178,11 @@ def particles_unnamed():
|
||||
|
||||
def textures():
|
||||
# returns the number of textures in the project
|
||||
|
||||
return len(bpy.data.textures)
|
||||
count = 0
|
||||
for texture in bpy.data.textures:
|
||||
if not compat.is_library_or_override(texture):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def textures_unused():
|
||||
@@ -174,8 +199,11 @@ def textures_unnamed():
|
||||
|
||||
def worlds():
|
||||
# returns the number of worlds in the project
|
||||
|
||||
return len(bpy.data.worlds)
|
||||
count = 0
|
||||
for world in bpy.data.worlds:
|
||||
if not compat.is_library_or_override(world):
|
||||
count += 1
|
||||
return count
|
||||
|
||||
|
||||
def worlds_unused():
|
||||
|
||||
@@ -25,6 +25,7 @@ project.
|
||||
|
||||
import bpy
|
||||
import os
|
||||
from ..utils import version, compat
|
||||
|
||||
|
||||
def get_missing(data):
|
||||
@@ -37,12 +38,29 @@ def get_missing(data):
|
||||
do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"]
|
||||
|
||||
for datablock in data:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(datablock):
|
||||
continue
|
||||
|
||||
# the absolute path to our data-block
|
||||
abspath = bpy.path.abspath(datablock.filepath)
|
||||
|
||||
# Check if data-block is packed
|
||||
# Blender 5.0+: Image objects use 'packed_files' (plural), Library objects use 'packed_file' (singular)
|
||||
# Blender 4.2/4.5: Both Image and Library objects use 'packed_file' (singular)
|
||||
is_packed = False
|
||||
if version.is_version_at_least(5, 0, 0):
|
||||
# Blender 5.0+: Check type-specific attributes
|
||||
if isinstance(datablock, bpy.types.Image):
|
||||
is_packed = bool(datablock.packed_files) if hasattr(datablock, 'packed_files') else False
|
||||
elif isinstance(datablock, bpy.types.Library):
|
||||
is_packed = bool(datablock.packed_file) if hasattr(datablock, 'packed_file') else False
|
||||
else:
|
||||
# Blender 4.2/4.5: Both use 'packed_file' (singular)
|
||||
is_packed = bool(datablock.packed_file) if hasattr(datablock, 'packed_file') else False
|
||||
|
||||
# if data-block is not packed and has an invalid filepath
|
||||
if not datablock.packed_files and not os.path.isfile(abspath):
|
||||
if not is_packed and not os.path.isfile(abspath):
|
||||
|
||||
# if data-block is not in our do not flag list
|
||||
# append it to the missing data list
|
||||
@@ -50,7 +68,7 @@ def get_missing(data):
|
||||
missing.append(datablock.name)
|
||||
|
||||
# if data-block is packed but it does not have a filepath
|
||||
elif datablock.packed_files and not abspath:
|
||||
elif is_packed and not abspath:
|
||||
|
||||
# if data-block is not in our do not flag list
|
||||
# append it to the missing data list
|
||||
|
||||
@@ -25,6 +25,7 @@ Blender project.
|
||||
|
||||
import bpy
|
||||
import re
|
||||
from ..utils import compat
|
||||
|
||||
|
||||
def collections():
|
||||
@@ -32,6 +33,9 @@ def collections():
|
||||
unnamed = []
|
||||
|
||||
for collection in bpy.data.collections:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(collection):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', collection.name) or \
|
||||
collection.name.startswith("Collection"):
|
||||
unnamed.append(collection.name)
|
||||
@@ -44,6 +48,9 @@ def images():
|
||||
unnamed = []
|
||||
|
||||
for image in bpy.data.images:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(image):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', image.name) or \
|
||||
image.name.startswith("Untitled"):
|
||||
unnamed.append(image.name)
|
||||
@@ -56,6 +63,9 @@ def lights():
|
||||
unnamed = []
|
||||
|
||||
for light in bpy.data.lights:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(light):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', light.name) or \
|
||||
light.name.startswith("Light"):
|
||||
unnamed.append(light.name)
|
||||
@@ -67,7 +77,10 @@ def materials():
|
||||
# returns the keys of all unnamed materials in the project
|
||||
unnamed = []
|
||||
|
||||
for material in bpy.data.lights:
|
||||
for material in bpy.data.materials:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(material):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', material.name) or \
|
||||
material.name.startswith("Material"):
|
||||
unnamed.append(material.name)
|
||||
@@ -152,6 +165,9 @@ def objects():
|
||||
unnamed = []
|
||||
|
||||
for obj in bpy.data.objects:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', obj.name) or \
|
||||
obj.name.startswith(default_obj_names):
|
||||
unnamed.append(obj.name)
|
||||
@@ -164,6 +180,9 @@ def node_groups():
|
||||
unnamed = []
|
||||
|
||||
for node_group in bpy.data.node_groups:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(node_group):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', node_group.name) or \
|
||||
node_group.name.startswith("NodeGroup"):
|
||||
unnamed.append(node_group.name)
|
||||
@@ -176,6 +195,9 @@ def particles():
|
||||
unnamed = []
|
||||
|
||||
for particle in bpy.data.particles:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(particle):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', particle.name) or \
|
||||
particle.name.startswith("ParticleSettings"):
|
||||
unnamed.append(particle.name)
|
||||
@@ -188,6 +210,9 @@ def textures():
|
||||
unnamed = []
|
||||
|
||||
for texture in bpy.data.textures:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(texture):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', texture.name) or \
|
||||
texture.name.startswith("Texture"):
|
||||
unnamed.append(texture.name)
|
||||
@@ -200,6 +225,9 @@ def worlds():
|
||||
unnamed = []
|
||||
|
||||
for world in bpy.data.worlds:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(world):
|
||||
continue
|
||||
if re.match(r'.*\.\d\d\d$', world.name) or \
|
||||
world.name.startswith("World"):
|
||||
unnamed.append(world.name)
|
||||
|
||||
@@ -25,6 +25,7 @@ as determined by stats.users.py
|
||||
|
||||
import bpy
|
||||
from .. import config
|
||||
from ..utils import compat
|
||||
from . import users
|
||||
|
||||
|
||||
@@ -35,6 +36,9 @@ def shallow(data):
|
||||
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
|
||||
@@ -52,6 +56,9 @@ def collections_deep():
|
||||
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)
|
||||
|
||||
@@ -65,6 +72,9 @@ def collections_shallow():
|
||||
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)
|
||||
|
||||
@@ -81,6 +91,9 @@ def images_deep():
|
||||
do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"]
|
||||
|
||||
for image in bpy.data.images:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(image):
|
||||
continue
|
||||
if not users.image_all(image.name):
|
||||
|
||||
# check if image has a fake user or if ignore fake users
|
||||
@@ -118,6 +131,9 @@ def lights_deep():
|
||||
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
|
||||
@@ -141,6 +157,9 @@ def materials_deep():
|
||||
unused = []
|
||||
|
||||
for material in bpy.data.materials:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(material):
|
||||
continue
|
||||
if not users.material_all(material.name):
|
||||
|
||||
# check if material has a fake user or if ignore fake users
|
||||
@@ -164,6 +183,9 @@ def node_groups_deep():
|
||||
unused = []
|
||||
|
||||
for node_group in bpy.data.node_groups:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(node_group):
|
||||
continue
|
||||
if not users.node_group_all(node_group.name):
|
||||
|
||||
# check if node group has a fake user or if ignore fake users
|
||||
@@ -190,6 +212,9 @@ def particles_deep():
|
||||
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
|
||||
@@ -216,6 +241,9 @@ def textures_deep():
|
||||
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
|
||||
@@ -239,6 +267,9 @@ def worlds():
|
||||
unused = []
|
||||
|
||||
for world in bpy.data.worlds:
|
||||
# Skip library-linked and override datablocks
|
||||
if compat.is_library_or_override(world):
|
||||
continue
|
||||
|
||||
# if data-block has no users or if it has a fake user and
|
||||
# ignore fake users is enabled
|
||||
@@ -248,3 +279,55 @@ def worlds():
|
||||
unused.append(world.name)
|
||||
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import bpy
|
||||
from ..stats import unused
|
||||
from ..stats import users
|
||||
from .. import config
|
||||
from ..utils import compat
|
||||
|
||||
|
||||
def get_all_unused_parallel():
|
||||
"""
|
||||
Get all unused data-blocks efficiently in a single batch.
|
||||
|
||||
Returns a dictionary with keys:
|
||||
- collections: list of unused collection names
|
||||
- images: list of unused image names
|
||||
- lights: list of unused light names
|
||||
- materials: list of unused material names
|
||||
- node_groups: list of unused node group names
|
||||
- objects: list of unused object names
|
||||
- particles: list of unused particle names
|
||||
- textures: list of unused texture names
|
||||
- armatures: list of unused armature names
|
||||
- worlds: list of unused world names
|
||||
"""
|
||||
# Execute all checks sequentially but in a clean batch
|
||||
# This avoids threading overhead while keeping code organized
|
||||
return {
|
||||
'collections': unused.collections_deep(),
|
||||
'images': unused.images_deep(),
|
||||
'lights': unused.lights_deep(),
|
||||
'materials': unused.materials_deep(),
|
||||
'node_groups': unused.node_groups_deep(),
|
||||
'objects': unused.objects_deep(),
|
||||
'particles': unused.particles_deep(),
|
||||
'textures': unused.textures_deep(),
|
||||
'armatures': unused.armatures_deep(),
|
||||
'worlds': unused.worlds(),
|
||||
}
|
||||
|
||||
|
||||
def _has_any_unused_collections():
|
||||
"""Check if there are any unused collections (short-circuits early)."""
|
||||
for collection in bpy.data.collections:
|
||||
if compat.is_library_or_override(collection):
|
||||
continue
|
||||
if not users.collection_all(collection.name):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_images():
|
||||
"""Check if there are any unused images (short-circuits early)."""
|
||||
do_not_flag = ["Render Result", "Viewer Node", "D-NOISE Export"]
|
||||
|
||||
for image in bpy.data.images:
|
||||
if compat.is_library_or_override(image):
|
||||
continue
|
||||
if not users.image_all(image.name):
|
||||
if not image.use_fake_user or config.include_fake_users:
|
||||
if image.name not in do_not_flag:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_lights():
|
||||
"""Check if there are any unused lights (short-circuits early)."""
|
||||
for light in bpy.data.lights:
|
||||
if compat.is_library_or_override(light):
|
||||
continue
|
||||
if not users.light_all(light.name):
|
||||
if not light.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_materials():
|
||||
"""Check if there are any unused materials (short-circuits early)."""
|
||||
for material in bpy.data.materials:
|
||||
if compat.is_library_or_override(material):
|
||||
continue
|
||||
if not users.material_all(material.name):
|
||||
if not material.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_node_groups():
|
||||
"""Check if there are any unused node groups (short-circuits early)."""
|
||||
for node_group in bpy.data.node_groups:
|
||||
if compat.is_library_or_override(node_group):
|
||||
continue
|
||||
if not users.node_group_all(node_group.name):
|
||||
if not node_group.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_particles():
|
||||
"""Check if there are any unused particles (short-circuits early)."""
|
||||
if not hasattr(bpy.data, 'particles'):
|
||||
return False
|
||||
|
||||
for particle in bpy.data.particles:
|
||||
if compat.is_library_or_override(particle):
|
||||
continue
|
||||
if not users.particle_all(particle.name):
|
||||
if not particle.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_textures():
|
||||
"""Check if there are any unused textures (short-circuits early)."""
|
||||
if not hasattr(bpy.data, 'textures'):
|
||||
return False
|
||||
|
||||
for texture in bpy.data.textures:
|
||||
if compat.is_library_or_override(texture):
|
||||
continue
|
||||
if not users.texture_all(texture.name):
|
||||
if not texture.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_worlds():
|
||||
"""Check if there are any unused worlds (short-circuits early)."""
|
||||
for world in bpy.data.worlds:
|
||||
if compat.is_library_or_override(world):
|
||||
continue
|
||||
if world.users == 0 or (world.users == 1 and
|
||||
world.use_fake_user and
|
||||
config.include_fake_users):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_objects():
|
||||
"""Check if there are any unused objects (short-circuits early)."""
|
||||
for obj in bpy.data.objects:
|
||||
if compat.is_library_or_override(obj):
|
||||
continue
|
||||
if not users.object_all(obj.name):
|
||||
if not obj.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_any_unused_armatures():
|
||||
"""Check if there are any unused armatures (short-circuits early)."""
|
||||
for armature in bpy.data.armatures:
|
||||
if compat.is_library_or_override(armature):
|
||||
continue
|
||||
if not users.armature_all(armature.name):
|
||||
if not armature.use_fake_user or config.include_fake_users:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def get_unused_for_smart_select():
|
||||
"""
|
||||
Get unused data for smart select operation (returns booleans).
|
||||
Optimized to short-circuit early - stops checking each category
|
||||
as soon as unused data is found. This is much faster than computing
|
||||
the full list of unused items.
|
||||
|
||||
Returns a dictionary with boolean values indicating if each category
|
||||
has unused data-blocks.
|
||||
"""
|
||||
# Use optimized short-circuit versions that stop as soon as
|
||||
# they find ONE unused item, rather than computing the full list
|
||||
return {
|
||||
'collections': _has_any_unused_collections(),
|
||||
'images': _has_any_unused_images(),
|
||||
'lights': _has_any_unused_lights(),
|
||||
'materials': _has_any_unused_materials(),
|
||||
'node_groups': _has_any_unused_node_groups(),
|
||||
'objects': _has_any_unused_objects(),
|
||||
'particles': _has_any_unused_particles(),
|
||||
'textures': _has_any_unused_textures(),
|
||||
'armatures': _has_any_unused_armatures(),
|
||||
'worlds': _has_any_unused_worlds(),
|
||||
}
|
||||
|
||||
|
||||
@@ -426,12 +426,24 @@ def light_objects(light_key):
|
||||
|
||||
def material_all(material_key):
|
||||
# returns a list of keys of every data-block that uses this material
|
||||
return material_objects(material_key) + \
|
||||
material_geometry_nodes(material_key)
|
||||
# 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))
|
||||
|
||||
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 = []
|
||||
material = bpy.data.materials[material_key]
|
||||
@@ -440,17 +452,104 @@ def material_geometry_nodes(material_key):
|
||||
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
|
||||
if node_group_has_material(ng.name, material.name):
|
||||
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
|
||||
material = bpy.data.materials[material_key]
|
||||
|
||||
# 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
|
||||
if node_group_has_material(node_group.name, material.name):
|
||||
# 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_objects(material_key):
|
||||
# returns a list of object keys that use this material
|
||||
|
||||
@@ -760,22 +859,122 @@ def node_group_has_material(node_group_key, material_key):
|
||||
# returns true if a node group contains this material (directly or nested)
|
||||
|
||||
has_material = False
|
||||
node_group = bpy.data.node_groups[node_group_key]
|
||||
material = bpy.data.materials[material_key]
|
||||
try:
|
||||
node_group = bpy.data.node_groups[node_group_key]
|
||||
material = bpy.data.materials[material_key]
|
||||
except (KeyError, AttributeError):
|
||||
return False
|
||||
|
||||
for node in node_group.nodes:
|
||||
# base case: nodes with a material property (e.g., Set Material)
|
||||
if hasattr(node, 'material') and node.material:
|
||||
if node.material.name == material.name:
|
||||
has_material = True
|
||||
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
|
||||
if socket_material and hasattr(socket_material, 'name'):
|
||||
if (socket_material.name == material.name or
|
||||
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
|
||||
if socket_material and hasattr(socket_material, 'name'):
|
||||
if (socket_material.name == material.name or
|
||||
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:
|
||||
if (node.material.name == material.name or
|
||||
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
|
||||
|
||||
# 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:
|
||||
# Check both by name and by direct reference for robustness
|
||||
if (node.material.name == material.name or
|
||||
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:
|
||||
if (node.material.name == material.name or
|
||||
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
|
||||
elif hasattr(node, 'node_tree') and node.node_tree:
|
||||
has_material = node_group_has_material(
|
||||
node.node_tree.name, material.name)
|
||||
# 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(
|
||||
node.node_tree.name, material.name)
|
||||
except (KeyError, AttributeError, ReferenceError, RuntimeError):
|
||||
continue # Skip invalid node groups
|
||||
|
||||
if has_material:
|
||||
break
|
||||
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
|
||||
|
||||
@@ -969,6 +1168,75 @@ def texture_particles(texture_key):
|
||||
return distinct(users)
|
||||
|
||||
|
||||
def object_all(object_key):
|
||||
# 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
|
||||
|
||||
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)
|
||||
|
||||
return distinct(users)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user