work: save startup because I'm sick of the arnoldesque materials

This commit is contained in:
2026-04-24 14:39:38 -06:00
parent b401eb4bcf
commit 2f8e5f472f
57 changed files with 2257 additions and 1643 deletions
@@ -0,0 +1,88 @@
"""
Detect 'ghost' ID references (e.g. Reallusion CC / iClone import caches on Scene)
that keep bpy.users > 0 but are not real scene usage. Used by materials_deep and
any future cleanup paths.
"""
import bpy
from .. import config
def _idprop_count_material(p, mat, seen):
"""Recursively count pointers equal to `mat` under an ID property value."""
if p is None or id(p) in seen:
return 0
if isinstance(p, (str, int, float, bool)):
return 0
seen = seen | {id(p)}
if isinstance(p, bpy.types.Material) and p == mat:
return 1
n = 0
t = type(p).__name__
if isinstance(p, (list, tuple)):
for x in p:
n += _idprop_count_material(x, mat, seen)
elif isinstance(p, dict):
for v in p.values():
n += _idprop_count_material(v, mat, seen)
elif t == "IDPropertyGroup" or (hasattr(p, "keys") and hasattr(p, "values") and t != "dict"):
try:
for k in p.keys():
try:
v = p[k]
except Exception:
continue
n += _idprop_count_material(v, mat, seen)
except Exception:
pass
return n
def count_cc3_import_cache_references(material):
"""
Count Material pointers stored in Scene ID property group CC3ImportProps
(Character Creator 3 / iClone pipeline). These are import-cache ghosts and
are not object/world/brush use.
"""
m = 0
for scene in bpy.data.scenes:
try:
if "CC3ImportProps" not in scene:
continue
except Exception:
continue
try:
cc3 = scene["CC3ImportProps"]
except Exception:
continue
m += _idprop_count_material(cc3, material, set())
return m
def material_blender_users_fully_cc3_ghosts(material):
"""
True if every material.user can be explained by CC3 import_cache pointers.
Blender often reports *two* users per pbr cache slot: the Scene and the
pbr_material_cache cell both contribute to bpy.types.Material.users, while
our walk only counts Material pointers in the idprop tree (one per cell).
So u == 2 * cc3 is normal; u == cc3 can occur when the count already matches
1:1 in some file versions.
"""
try:
u = material.users
except Exception:
return False
if u == 0:
return True
cc3 = count_cc3_import_cache_references(material)
if cc3 == 0:
return False
if cc3 == u or u == 2 * cc3:
return True
if config.enable_debug_prints:
config.debug_print(
f"[Atomic Debug] ghost_users: material '{material.name}' users={u} "
f"cc3_count={cc3} (not fully explained by CC3; keep conservative block)"
)
return False
@@ -28,6 +28,7 @@ import json
import os
from .. import config
from ..utils import compat
from . import ghost_users
# Data-block types we care about for dependency analysis
@@ -1197,7 +1198,9 @@ def analyze_unused_from_graph(graph, category, include_fake_users=None):
if category == 'materials':
try:
if datablock.users > 0 and not datablock.use_fake_user:
continue
if not ghost_users.material_blender_users_fully_cc3_ghosts(
datablock):
continue
except (AttributeError, RuntimeError, ReferenceError):
pass
unused.append(item_name)
@@ -28,6 +28,7 @@ from .. import config
from ..utils import compat
from ..utils import version
from . import users
from . import ghost_users
def shallow(data):
@@ -223,10 +224,10 @@ def materials_deep():
# 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:
# If Blender still counts users but we found none, don't flag (name collisions
# with linked IDs, drivers, or refs we don't traverse). Fake-user purge unchanged.
if material.users > 0 and not material.use_fake_user:
continue
# CC3 / iClone import_cache ID props keep bpy.users>0 with no object/world use.
if not ghost_users.material_blender_users_fully_cc3_ghosts(material):
continue
unused.append(material.name)
else:
# Second check: material is used, but check if it's ONLY used by unused objects
@@ -1,6 +1,7 @@
import bpy
from ..stats import unused
from ..stats import users
from . import ghost_users
from .. import config
from ..utils import compat
@@ -128,7 +129,8 @@ def _has_any_unused_materials():
if not users.material_all(material.name):
if not material.use_fake_user or config.include_fake_users:
if material.users > 0 and not material.use_fake_user:
continue
if not ghost_users.material_blender_users_fully_cc3_ghosts(material):
continue
return True
else:
# Second check: material is used, but check if it's ONLY used by unused objects