update addons (for 5.2)
This commit is contained in:
@@ -4,6 +4,7 @@ between Blender 4.2 LTS, 4.5 LTS, and 5.0.
|
||||
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import os
|
||||
import bpy
|
||||
from bpy.utils import register_class, unregister_class
|
||||
@@ -220,6 +221,31 @@ def _mesh_vertex_sum_sample():
|
||||
return s
|
||||
|
||||
|
||||
def _cache_modifier_counts():
|
||||
"""Cheap counts so storage cache invalidates when physics/GeoNodes mods change."""
|
||||
nodes = cloth = soft = dp = particles = 0
|
||||
for ob in bpy.data.objects:
|
||||
try:
|
||||
for mod in ob.modifiers:
|
||||
t = getattr(mod, "type", None)
|
||||
if t == "NODES":
|
||||
nodes += 1
|
||||
elif t == "CLOTH":
|
||||
cloth += 1
|
||||
elif t == "SOFT_BODY":
|
||||
soft += 1
|
||||
elif t == "DYNAMIC_PAINT":
|
||||
dp += 1
|
||||
particles += len(getattr(ob, "particle_systems", []))
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
pass
|
||||
rb = 0
|
||||
for sc in bpy.data.scenes:
|
||||
if getattr(sc, "rigidbody_world", None):
|
||||
rb += 1
|
||||
return (nodes, cloth, soft, dp, particles, rb)
|
||||
|
||||
|
||||
def _light_fingerprint():
|
||||
fp = bpy.data.filepath
|
||||
try:
|
||||
@@ -236,6 +262,7 @@ def _light_fingerprint():
|
||||
len(bpy.data.objects),
|
||||
len(bpy.data.armatures),
|
||||
len(bpy.data.collections),
|
||||
_cache_modifier_counts(),
|
||||
)
|
||||
|
||||
|
||||
@@ -499,6 +526,269 @@ def format_bytes(n):
|
||||
return _fmt_bytes(n)
|
||||
|
||||
|
||||
# DNA layout of blender::NodesModifierBake (64-bit). bake_size is not in RNA.
|
||||
# See source/blender/makesdna/DNA_modifier_types.h
|
||||
class _NodesModifierBakeDNA(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("id", ctypes.c_int32),
|
||||
("flag", ctypes.c_uint32),
|
||||
("bake_mode", ctypes.c_uint8),
|
||||
("bake_target", ctypes.c_int8),
|
||||
("_pad", ctypes.c_char * 6),
|
||||
("directory", ctypes.c_void_p),
|
||||
("frame_start", ctypes.c_int32),
|
||||
("frame_end", ctypes.c_int32),
|
||||
("data_blocks_num", ctypes.c_int32),
|
||||
("active_data_block", ctypes.c_int32),
|
||||
("data_blocks", ctypes.c_void_p),
|
||||
("packed", ctypes.c_void_p),
|
||||
("_pad2", ctypes.c_void_p),
|
||||
("bake_size", ctypes.c_int64),
|
||||
]
|
||||
|
||||
|
||||
_PHYS_BYTES_PER_POINT = 32
|
||||
|
||||
|
||||
def _geonodes_bake_dna_sizes(bake):
|
||||
"""
|
||||
Read packed pointer and bake_size from NodesModifierBake DNA.
|
||||
Returns (bake_size, is_packed) or None if unavailable.
|
||||
"""
|
||||
if bake is None or ctypes.sizeof(ctypes.c_void_p) != 8:
|
||||
return None
|
||||
try:
|
||||
ptr = bake.as_pointer()
|
||||
if not ptr:
|
||||
return None
|
||||
dna = _NodesModifierBakeDNA.from_address(ptr)
|
||||
size = int(dna.bake_size)
|
||||
packed = bool(dna.packed)
|
||||
if size < 0:
|
||||
return None
|
||||
return (size, packed)
|
||||
except (AttributeError, TypeError, ValueError, OSError, RuntimeError):
|
||||
return None
|
||||
|
||||
|
||||
def _append_geonodes_bake_rows(rows, is_override):
|
||||
"""Append rows for GeoNodes bakes packed into the .blend."""
|
||||
# packed + bake_size DNA fields exist since Blender 4.3
|
||||
if not version.is_version_at_least(4, 3):
|
||||
return
|
||||
for ob in bpy.data.objects:
|
||||
if _skip_linked(ob):
|
||||
continue
|
||||
try:
|
||||
mods = ob.modifiers
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
continue
|
||||
io = is_override(ob)
|
||||
ow = 0.08 if io else 1.0
|
||||
for mod in mods:
|
||||
if not is_geometry_nodes_modifier(mod):
|
||||
continue
|
||||
bakes = getattr(mod, "bakes", None)
|
||||
if not bakes:
|
||||
continue
|
||||
for bake in bakes:
|
||||
info = _geonodes_bake_dna_sizes(bake)
|
||||
if info is None:
|
||||
continue
|
||||
size, packed = info
|
||||
if not packed or size <= 0:
|
||||
continue
|
||||
node = getattr(bake, "node", None)
|
||||
node_name = getattr(node, "name", None) if node else None
|
||||
label = node_name or ("Bake %s" % getattr(bake, "bake_id", "?"))
|
||||
name = "%s / %s / %s" % (ob.name, mod.name, label)
|
||||
emb = max(1, int(size * ow))
|
||||
rows.append(
|
||||
{
|
||||
"type": "GeoNodesBake",
|
||||
"name": name,
|
||||
"embedded": emb,
|
||||
"size_bytes": emb,
|
||||
"is_lib_override": io,
|
||||
"kind": "packed",
|
||||
"owner_object": ob.name,
|
||||
"modifier_name": mod.name,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _point_cache_in_blend(cache):
|
||||
"""True when a point cache is baked into the .blend (not disk/external)."""
|
||||
if cache is None:
|
||||
return False
|
||||
try:
|
||||
if not cache.is_baked:
|
||||
return False
|
||||
if getattr(cache, "use_disk_cache", False):
|
||||
return False
|
||||
if getattr(cache, "use_external", False):
|
||||
return False
|
||||
return True
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
return False
|
||||
|
||||
|
||||
def _iter_point_cache_items(pc):
|
||||
if pc is None:
|
||||
return
|
||||
items = getattr(pc, "point_caches", None)
|
||||
try:
|
||||
if items is not None and len(items) > 0:
|
||||
for item in items:
|
||||
yield item
|
||||
return
|
||||
except (AttributeError, RuntimeError, TypeError):
|
||||
pass
|
||||
yield pc
|
||||
|
||||
|
||||
def _physics_cache_size_bytes(cache, point_count):
|
||||
try:
|
||||
fs = int(cache.frame_start)
|
||||
fe = int(cache.frame_end)
|
||||
step = max(1, int(getattr(cache, "frame_step", 1) or 1))
|
||||
except (AttributeError, TypeError, ValueError, RuntimeError):
|
||||
return None
|
||||
frames = max(1, (fe - fs) // step + 1)
|
||||
pts = max(1, int(point_count) if point_count else 1)
|
||||
return max(64, frames * pts * _PHYS_BYTES_PER_POINT)
|
||||
|
||||
|
||||
def _object_mesh_vert_count(ob):
|
||||
data = getattr(ob, "data", None)
|
||||
if data is None:
|
||||
return 1
|
||||
try:
|
||||
return max(1, len(data.vertices))
|
||||
except (AttributeError, RuntimeError, ReferenceError, TypeError):
|
||||
return 1
|
||||
|
||||
|
||||
def _append_physics_cache_row(
|
||||
rows, name, cache, point_count, is_lib_override, owner_object="", owner_scene=""
|
||||
):
|
||||
if not _point_cache_in_blend(cache):
|
||||
return
|
||||
sz = _physics_cache_size_bytes(cache, point_count)
|
||||
if sz is None:
|
||||
return
|
||||
ow = 0.08 if is_lib_override else 1.0
|
||||
sz = max(64, int(sz * ow))
|
||||
cname = getattr(cache, "name", "") or ""
|
||||
display = "%s (%s)" % (name, cname) if cname else name
|
||||
row = {
|
||||
"type": "PhysicsCache",
|
||||
"name": display,
|
||||
"embedded": 0,
|
||||
"size_bytes": sz,
|
||||
"is_lib_override": is_lib_override,
|
||||
"kind": "override" if is_lib_override else "local",
|
||||
}
|
||||
if owner_object:
|
||||
row["owner_object"] = owner_object
|
||||
if owner_scene:
|
||||
row["owner_scene"] = owner_scene
|
||||
rows.append(row)
|
||||
|
||||
|
||||
def _append_physics_cache_rows(rows, is_override):
|
||||
"""Append rows for physics point caches stored inside the .blend."""
|
||||
for ob in bpy.data.objects:
|
||||
if _skip_linked(ob):
|
||||
continue
|
||||
io = is_override(ob)
|
||||
verts = _object_mesh_vert_count(ob)
|
||||
try:
|
||||
mods = ob.modifiers
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
mods = []
|
||||
for mod in mods:
|
||||
mtype = getattr(mod, "type", None)
|
||||
if mtype == "CLOTH":
|
||||
pc = getattr(mod, "point_cache", None)
|
||||
for item in _iter_point_cache_items(pc):
|
||||
_append_physics_cache_row(
|
||||
rows, "%s / Cloth" % ob.name, item, verts, io, owner_object=ob.name
|
||||
)
|
||||
elif mtype == "SOFT_BODY":
|
||||
pc = getattr(mod, "point_cache", None)
|
||||
for item in _iter_point_cache_items(pc):
|
||||
_append_physics_cache_row(
|
||||
rows, "%s / Soft Body" % ob.name, item, verts, io, owner_object=ob.name
|
||||
)
|
||||
elif mtype == "DYNAMIC_PAINT":
|
||||
canvas = getattr(mod, "canvas_settings", None)
|
||||
surfaces = getattr(canvas, "canvas_surfaces", None) if canvas else None
|
||||
if not surfaces:
|
||||
continue
|
||||
for surf in surfaces:
|
||||
pc = getattr(surf, "point_cache", None)
|
||||
fmt = getattr(surf, "surface_format", None)
|
||||
if fmt == "IMAGE":
|
||||
res = int(getattr(surf, "image_resolution", 256) or 256)
|
||||
pts = max(1, res * res)
|
||||
else:
|
||||
pts = verts
|
||||
sname = getattr(surf, "name", "Surface") or "Surface"
|
||||
for item in _iter_point_cache_items(pc):
|
||||
_append_physics_cache_row(
|
||||
rows,
|
||||
"%s / Dynamic Paint / %s" % (ob.name, sname),
|
||||
item,
|
||||
pts,
|
||||
io,
|
||||
owner_object=ob.name,
|
||||
)
|
||||
try:
|
||||
psystems = ob.particle_systems
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
psystems = []
|
||||
for ps in psystems:
|
||||
pc = getattr(ps, "point_cache", None)
|
||||
settings = getattr(ps, "settings", None)
|
||||
count = 1
|
||||
if settings is not None:
|
||||
try:
|
||||
count = max(1, int(getattr(settings, "count", 1) or 1))
|
||||
except (TypeError, ValueError):
|
||||
count = 1
|
||||
ps_name = getattr(ps, "name", "Particles") or "Particles"
|
||||
for item in _iter_point_cache_items(pc):
|
||||
_append_physics_cache_row(
|
||||
rows, "%s / %s" % (ob.name, ps_name), item, count, io, owner_object=ob.name
|
||||
)
|
||||
|
||||
for sc in bpy.data.scenes:
|
||||
if _skip_linked(sc):
|
||||
continue
|
||||
rbw = getattr(sc, "rigidbody_world", None)
|
||||
if not rbw:
|
||||
continue
|
||||
pc = getattr(rbw, "point_cache", None)
|
||||
coll = getattr(rbw, "collection", None)
|
||||
nobj = 1
|
||||
if coll is not None:
|
||||
try:
|
||||
nobj = max(1, len(coll.objects))
|
||||
except (AttributeError, RuntimeError, ReferenceError):
|
||||
nobj = 1
|
||||
io = is_override(sc)
|
||||
for item in _iter_point_cache_items(pc):
|
||||
_append_physics_cache_row(
|
||||
rows,
|
||||
"%s / Rigid Body" % sc.name,
|
||||
item,
|
||||
nobj,
|
||||
io,
|
||||
owner_scene=sc.name,
|
||||
)
|
||||
|
||||
|
||||
_STORAGE_TYPE_ICONS = {
|
||||
"Mesh": "MESH_DATA",
|
||||
"Image": "IMAGE_DATA",
|
||||
@@ -514,6 +804,8 @@ _STORAGE_TYPE_ICONS = {
|
||||
"Sound": "SOUND",
|
||||
"Font": "FONT_DATA",
|
||||
"Collection": "OUTLINER_COLLECTION",
|
||||
"GeoNodesBake": "GEOMETRY_NODES",
|
||||
"PhysicsCache": "PHYSICS",
|
||||
}
|
||||
|
||||
|
||||
@@ -522,8 +814,13 @@ def storage_type_icon(type_name):
|
||||
return _STORAGE_TYPE_ICONS.get(type_name, "BLANK1")
|
||||
|
||||
|
||||
def storage_packed_icon(type_name):
|
||||
"""Packed-in-blend indicator (GeoNodes bake rows only)."""
|
||||
return "PACKAGE" if type_name == "GeoNodesBake" else "BLANK1"
|
||||
|
||||
|
||||
def storage_override_icon(is_lib_override):
|
||||
"""Second column: library override emblem vs empty spacer."""
|
||||
"""Library override emblem vs empty spacer."""
|
||||
return "LIBRARY_DATA_OVERRIDE" if is_lib_override else "BLANK1"
|
||||
|
||||
|
||||
@@ -758,6 +1055,9 @@ def build_report():
|
||||
}
|
||||
)
|
||||
|
||||
_append_geonodes_bake_rows(rows, _ov)
|
||||
_append_physics_cache_rows(rows, _ov)
|
||||
|
||||
rows.sort(key=lambda r: r["size_bytes"], reverse=True)
|
||||
|
||||
by_type = {}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Navigate from Atomic Storage rows to datablock users in the Blender UI.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from .. import config
|
||||
from ..stats import users
|
||||
|
||||
|
||||
def _find_area(context, area_type):
|
||||
screen = context.screen
|
||||
if context.window:
|
||||
screen = context.window.screen
|
||||
for area in screen.areas:
|
||||
if area.type == area_type:
|
||||
region = next((r for r in area.regions if r.type == "WINDOW"), None)
|
||||
return context.window, area, region
|
||||
return context.window, None, None
|
||||
|
||||
|
||||
def resolve_targets(storage_type, id_name, owner_object="", owner_scene="", modifier_name=""):
|
||||
"""
|
||||
Return navigation targets as dicts:
|
||||
object, material, action, nav (viewport|shader|dopesheet).
|
||||
"""
|
||||
targets = []
|
||||
|
||||
def add_objects(names, nav="viewport", material="", action=""):
|
||||
for name in names:
|
||||
if name in bpy.data.objects:
|
||||
targets.append(
|
||||
{
|
||||
"object": name,
|
||||
"material": material,
|
||||
"action": action,
|
||||
"nav": nav,
|
||||
"modifier": modifier_name if owner_object == name else "",
|
||||
}
|
||||
)
|
||||
|
||||
if storage_type == "Object":
|
||||
if id_name in bpy.data.objects:
|
||||
add_objects([id_name])
|
||||
return targets
|
||||
|
||||
if storage_type == "GeoNodesBake":
|
||||
if owner_object:
|
||||
add_objects([owner_object])
|
||||
return targets
|
||||
|
||||
if storage_type == "PhysicsCache":
|
||||
if owner_object:
|
||||
add_objects([owner_object])
|
||||
return targets
|
||||
if owner_scene:
|
||||
scene = bpy.data.scenes.get(owner_scene)
|
||||
rbw = getattr(scene, "rigidbody_world", None) if scene else None
|
||||
coll = getattr(rbw, "collection", None) if rbw else None
|
||||
if coll:
|
||||
add_objects([ob.name for ob in coll.objects])
|
||||
return targets
|
||||
|
||||
if storage_type == "Mesh":
|
||||
add_objects(users.mesh_objects(id_name))
|
||||
elif storage_type == "Curve":
|
||||
add_objects(users.curve_objects(id_name))
|
||||
elif storage_type == "Armature":
|
||||
add_objects(users.armature_all(id_name))
|
||||
elif storage_type == "Volume":
|
||||
add_objects(users.volume_objects(id_name))
|
||||
elif storage_type == "PointCloud":
|
||||
add_objects(users.pointcloud_objects(id_name))
|
||||
elif storage_type == "Image":
|
||||
add_objects(users.image_viewport_objects(id_name))
|
||||
elif storage_type == "Material":
|
||||
seen = set()
|
||||
for name in users.material_objects(id_name):
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
add_objects([name], nav="shader", material=id_name)
|
||||
for name in users.material_geometry_nodes(id_name):
|
||||
if name not in seen:
|
||||
seen.add(name)
|
||||
add_objects([name], nav="shader", material=id_name)
|
||||
elif storage_type == "Action":
|
||||
for name in users.action_objects(id_name):
|
||||
add_objects([name], nav="dopesheet", action=id_name)
|
||||
elif storage_type == "NodeTree":
|
||||
add_objects(users.node_group_viewport_objects(id_name))
|
||||
elif storage_type == "Texture":
|
||||
add_objects(users.texture_objects(id_name))
|
||||
elif storage_type == "Collection":
|
||||
add_objects(users.collection_viewport_objects(id_name))
|
||||
elif storage_type == "Sound":
|
||||
add_objects(users.sound_viewport_objects(id_name))
|
||||
elif storage_type == "Font":
|
||||
add_objects(users.font_viewport_objects(id_name))
|
||||
|
||||
return targets
|
||||
|
||||
|
||||
def _select_object(context, object_name):
|
||||
vl = context.view_layer
|
||||
if object_name not in vl.objects:
|
||||
return False
|
||||
for ob in vl.objects:
|
||||
ob.select_set(False)
|
||||
ob = vl.objects[object_name]
|
||||
ob.select_set(True)
|
||||
vl.objects.active = ob
|
||||
return True
|
||||
|
||||
|
||||
def _frame_view3d(context):
|
||||
window, area, region = _find_area(context, "VIEW_3D")
|
||||
if not area or not region:
|
||||
return
|
||||
with context.temp_override(window=window, area=area, region=region):
|
||||
try:
|
||||
bpy.ops.view3d.view_selected()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
||||
def _outliner_show_active(context):
|
||||
window, area, region = _find_area(context, "OUTLINER")
|
||||
if not area or not region:
|
||||
return
|
||||
with context.temp_override(window=window, area=area, region=region):
|
||||
try:
|
||||
bpy.ops.outliner.show_active()
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
||||
def _focus_shader_editor(context, material_name):
|
||||
material = bpy.data.materials.get(material_name)
|
||||
if not material:
|
||||
return
|
||||
ob = context.view_layer.objects.active
|
||||
if ob and hasattr(ob, "material_slots"):
|
||||
for i, slot in enumerate(ob.material_slots):
|
||||
if slot.material == material:
|
||||
ob.active_material_index = i
|
||||
break
|
||||
window, area, _region = _find_area(context, "NODE_EDITOR")
|
||||
if not area:
|
||||
return
|
||||
space = area.spaces.active
|
||||
if space.type != "NODE_EDITOR":
|
||||
return
|
||||
space.tree_type = "ShaderNodeTree"
|
||||
if hasattr(space, "shader_type"):
|
||||
space.shader_type = "OBJECT"
|
||||
if hasattr(space, "pin"):
|
||||
space.pin = False
|
||||
|
||||
|
||||
def _focus_dopesheet(context, action_name):
|
||||
action = bpy.data.actions.get(action_name)
|
||||
window, area, _region = _find_area(context, "DOPESHEET_EDITOR")
|
||||
if not area:
|
||||
return
|
||||
space = area.spaces.active
|
||||
if space.type != "DOPESHEET_EDITOR":
|
||||
return
|
||||
if action and hasattr(space, "action"):
|
||||
space.action = action
|
||||
|
||||
|
||||
def apply_target(context, target):
|
||||
"""Select object and open the appropriate editor for one target."""
|
||||
if not _select_object(context, target["object"]):
|
||||
return False
|
||||
|
||||
nav = target.get("nav", "viewport")
|
||||
if nav == "shader" and target.get("material"):
|
||||
_focus_shader_editor(context, target["material"])
|
||||
elif nav == "dopesheet" and target.get("action"):
|
||||
_focus_dopesheet(context, target["action"])
|
||||
|
||||
if config.storage_navigate_frame_view:
|
||||
_frame_view3d(context)
|
||||
_outliner_show_active(context)
|
||||
return True
|
||||
Reference in New Issue
Block a user