Files
blender-portable-repo/scripts/addons/cache_manager/opsdata.py
T
2026-03-17 14:58:51 -06:00

954 lines
26 KiB
Python

# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import re
import os
from pathlib import Path
from typing import List, Tuple, Generator, Dict, Union, Any, Optional
import bpy
from . import cmglobals
from .logger import LoggerFactory, log_new_lines
from .models import FolderListModel
logger = LoggerFactory.getLogger(__name__)
VERSION_DIR_MODEL = FolderListModel()
_cachefiles_enum_list: List[Tuple[str, str, str]] = []
_versions_enum_list: List[Tuple[str, str, str]] = []
_version_dir_model_init: bool = False
def init_version_dir_model(
context: bpy.types.Context,
) -> None:
global VERSION_DIR_MODEL
global _version_dir_model_init
# Is None if invalid.
if not context.scene.cm.cache_version_dir_path:
logger.error(
"Failed to initialize version directory model. Invalid path. Check addon preferences."
)
return
cache_version_dir = Path(context.scene.cm.cache_version_dir_path)
VERSION_DIR_MODEL.reset()
VERSION_DIR_MODEL.root_path = cache_version_dir
if context.scene.cm.category == "EXPORT":
if not VERSION_DIR_MODEL.items:
VERSION_DIR_MODEL.append_item("v001")
_version_dir_model_init = True
def get_version(str_value: str, format: type = str) -> Union[str, int, None]:
match = re.search(cmglobals._VERSION_PATTERN, str_value)
if match:
version = match.group()
if format == str:
return version
if format == int:
return int(version.replace("v", ""))
return None
def add_version_increment() -> str:
items = VERSION_DIR_MODEL.items # should be already sorted
versions = [get_version(item) for item in items if get_version(item)]
if len(versions) > 0:
latest_version = items[0]
increment = "v{:03}".format(int(latest_version.replace("v", "")) + 1)
else:
increment = "v001"
VERSION_DIR_MODEL.append_item(increment)
return increment
def get_versions_enum_list(
self: Any,
context: bpy.types.Context,
) -> List[Tuple[str, str, str]]:
global _versions_enum_list
global VERSION_DIR_MODEL
global init_version_dir_model
# Init model if it did not happen.
if not _version_dir_model_init:
init_version_dir_model(context)
# Clear all versions in enum list.
_versions_enum_list.clear()
_versions_enum_list.extend(VERSION_DIR_MODEL.items_as_enum_list)
return _versions_enum_list
def add_version_custom(custom_version: str) -> None:
global _versions_enum_list
global VERSION_DIR_MODEL
VERSION_DIR_MODEL.append_item(custom_version)
def _get_cachefiles(cachedir_path: Path, file_ext: str = ".abc") -> List[Path]:
if file_ext == ".*":
return [Path(f) for f in cachedir_path.iterdir() if f.is_file()]
else:
return [
Path(f)
for f in cachedir_path.iterdir()
if f.is_file() and f.suffix == file_ext
]
def get_cachefiles_enum(
self: bpy.types.Operator, context: bpy.types.Context
) -> List[Tuple[str, str, str]]:
_cachefiles_enum_list.clear()
if not context.scene.cm.is_cachedir_valid:
return _cachefiles_enum_list
_cachefiles_enum_list.extend(
[
(path.as_posix(), path.name, "")
for path in _get_cachefiles(context.scene.cm.cachedir_path)
]
)
return _cachefiles_enum_list
def traverse_collection_tree(
collection: bpy.types.Collection,
) -> Generator[bpy.types.Collection, None, None]:
yield collection
for child in collection.children:
yield from traverse_collection_tree(child)
def _print_log_list(log_list: Dict[str, List[str]], header_str: str) -> None:
if log_list:
log_new_lines(1)
text = [f"{obj_name}:\n{''.join(log_list[obj_name])}" for obj_name in log_list]
logger.info("%s\n%s", header_str, "".join(text))
def _append_str_to_log_list(
log_list: Dict[str, List[str]], obj_name: str, str_value: str
) -> Dict[str, List[str]]:
log_list.setdefault(obj_name, [])
log_list[obj_name].append(f"{str_value},\n")
return log_list
def disable_vis_drivers(
objects: List[bpy.types.Object], modifiers: bool = True
) -> List[bpy.types.Driver]:
# Store driver that were muted to entmute them after.
muted_drivers: List[bpy.types.Driver] = []
# Log list.
log_list: Dict[str, List[str]] = {}
for obj in objects:
if obj.animation_data:
for driver in obj.animation_data.drivers:
# Get suffix of data path, if modifiers modifier name is at the beginning.
data_path_split = driver.data_path.split(".")
data_path_suffix = data_path_split[-1]
# If modifiers == False do not adjust drivers of which the data
# paths are starting with modifiers.
if not modifiers:
if len(data_path_split) > 1:
if data_path_split[0].startswith("modifiers"):
continue
# Only disable drivers that drive visibility data paths.
if data_path_suffix not in cmglobals.DRIVER_VIS_DATA_PATHS:
continue
# If muted already continue.
if driver.mute == True:
continue
# Mute.
driver.mute = True
muted_drivers.append(driver)
# Populate log list.
_append_str_to_log_list(log_list, obj.name, driver.data_path)
# Log.
_print_log_list(log_list, "Disable visibility drivers:")
return muted_drivers
def disable_drivers_by_data_path(
objects: List[bpy.types.Object], data_path: str
) -> List[bpy.types.Driver]:
# Store driver that were muted to entmute them after.
muted_drivers: List[bpy.types.Driver] = []
# Log list.
log_list: Dict[str, List[str]] = {}
for obj in objects:
if obj.animation_data:
for driver in obj.animation_data.drivers:
if driver.data_path != data_path:
continue
# Skip if driver already muted.
if driver.mute == True:
continue
# Mute.
driver.mute = True
muted_drivers.append(driver)
# Populate log list.
_append_str_to_log_list(log_list, obj.name, driver.data_path)
return muted_drivers
def sync_modifier_vis_with_render_setting(
objs: List[bpy.types.Object],
) -> List[Tuple[bpy.types.Modifier, bool, bool]]:
mods_vis_override: List[Tuple[bpy.types.Modifier, bool, bool]] = []
log_list: Dict[str, List[str]] = {}
for obj in objs:
for mod in obj.modifiers:
# Do not affect those for export.
if mod.type in cmglobals.MODIFIERS_KEEP:
continue
# If already synced continue.
if mod.show_viewport == mod.show_render:
continue
# Save cache for reconstruction later.
show_viewport_cache = mod.show_viewport
show_render_cache = mod.show_render
# Sync show_viewport with show_render setting.
mod.show_viewport = mod.show_render
mods_vis_override.append((mod, show_viewport_cache, show_render_cache))
# Populate log list.
_append_str_to_log_list(
log_list,
obj.name,
f"{mod.name}: V: {show_viewport_cache} -> {mod.show_viewport}",
)
# Log.
_print_log_list(log_list, "Sync modifier viewport vis with render vis:")
return mods_vis_override
def apply_modifier_suffix_vis_override(
objs: List[bpy.types.Object], category: str
) -> List[Tuple[bpy.types.Modifier, bool, bool]]:
mods_vis_override: List[Tuple[bpy.types.Modifier, bool, bool]] = []
log_list: Dict[str, List[str]] = {}
for obj in objs:
for mod in list(obj.modifiers):
show_viewport_cache = mod.show_viewport
show_render_cache = mod.show_render
if category == "EXPORT":
if mod.name.endswith(cmglobals.CACHE_OFF_SUFFIX):
if mod.show_viewport == False and mod.show_render == False:
continue
mod.show_viewport = False
mod.show_render = False
elif mod.name.endswith(cmglobals.CACHE_ON_SUFFIX):
if mod.show_viewport == True and mod.show_render == True:
continue
mod.show_viewport = True
mod.show_render = True
else:
continue
if category == "IMPORT":
if mod.name.endswith(cmglobals.CACHE_OFF_SUFFIX):
if mod.show_viewport == True and mod.show_render == True:
continue
mod.show_viewport = True
mod.show_render = True
elif mod.name.endswith(cmglobals.CACHE_ON_SUFFIX):
if mod.show_viewport == False and mod.show_render == False:
continue
mod.show_viewport = False
mod.show_render = False
else:
continue
mods_vis_override.append((mod, show_viewport_cache, show_render_cache))
# Populate log list.
_append_str_to_log_list(
log_list,
obj.name,
f"{mod.name}: V: {show_viewport_cache} -> {mod.show_viewport} R: {show_render_cache} -> {mod.show_render}",
)
# Log.
_print_log_list(log_list, "Apply modifier suffix vis override:")
return mods_vis_override
def restore_modifier_vis(
modifiers: List[Tuple[bpy.types.Modifier, bool, bool]]
) -> None:
log_list: Dict[str, List[str]] = {}
for mod, show_viewport, show_render in modifiers:
if mod.show_viewport == show_viewport and mod.show_render == show_render:
continue
show_viewport_cache = mod.show_viewport
show_render_cache = mod.show_render
mod.show_viewport = show_viewport
mod.show_render = show_render
# Populate log list.
_append_str_to_log_list(
log_list,
mod.id_data.name,
f"{mod.name}: V: {show_viewport_cache} -> {mod.show_viewport} R: {show_render_cache} -> {mod.show_render}",
)
# Log.
_print_log_list(log_list, "Restore modifier visiblity:")
def config_modifiers_keep_state(
objs: List[bpy.types.Object],
enable: bool = True,
) -> List[Tuple[bpy.types.Modifier, bool, bool]]:
mods_vis_override: List[Tuple[bpy.types.Modifier, bool, bool]] = []
noun = "Enabled" if enable else "Disabled"
log_list: Dict[str, List[str]] = {}
for obj in objs:
for mod in list(obj.modifiers):
if mod.type not in cmglobals.MODIFIERS_KEEP:
continue
show_viewport_cache = mod.show_viewport
show_render_cache = mod.show_render
if enable:
if mod.show_viewport == True and mod.show_render == True:
continue
# Do not change viewport setting on enable, might create overhead
# for mods that are only needed for render.
mod.show_render = True
else:
if mod.show_viewport == False and mod.show_render == False:
continue
mod.show_viewport = False
mod.show_render = False
mods_vis_override.append((mod, show_viewport_cache, show_render_cache))
# Populate log list.
_append_str_to_log_list(
log_list,
obj.name,
mod.name,
)
# Log.
_print_log_list(log_list, f"{noun} modifiers:")
return mods_vis_override
def set_item_vis(
items: List[Union[bpy.types.Object, bpy.types.Collection]],
show: bool,
) -> List[Tuple[Union[bpy.types.Object, bpy.types.Collection], bool, bool]]:
items_vis: List[
Tuple[Union[bpy.types.Object, bpy.types.Collection], bool, bool]
] = []
hide = not show
noun = "Hide" if hide else "Show"
for item in items:
hide_viewport_cache = item.hide_viewport
hide_render_cache = item.hide_render
if hide:
if item.hide_viewport and item.hide_render:
continue
item.hide_render = True
item.hide_viewport = True
else:
if not item.hide_viewport and not item.hide_render:
continue
item.hide_render = False
item.hide_viewport = False
items_vis.append((item, hide_viewport_cache, hide_render_cache))
if items_vis:
# Log.
logger.info(
"%s:\n%s",
noun,
",\n".join([item.name for item, hv, hr in items_vis]),
)
return items_vis
def restore_item_vis(
item_vis_list: List[
Tuple[Union[bpy.types.Object, bpy.types.Collection], bool, bool]
]
) -> None:
log_list: Dict[str, List[str]] = {}
for item, hide_viewport, hide_render in item_vis_list:
if item.hide_viewport == hide_viewport and item.hide_render == hide_render:
continue
hide_viewport_cache = item.hide_viewport
hide_render_cache = item.hide_render
item.hide_viewport = hide_viewport
item.hide_render = hide_render
# Populate log list.
_append_str_to_log_list(
log_list,
item.name,
f"V: {not hide_viewport_cache} -> {not item.hide_viewport} R: {not hide_render_cache} -> {not item.hide_render}",
)
# Log.
_print_log_list(log_list, "Restore visibility:")
def get_layer_colls_from_colls(
context: bpy.types.Context, collections: List[bpy.types.Collection]
) -> List[bpy.types.LayerCollection]:
layer_colls: List[bpy.types.LayerCollection] = []
coll_name: List[str] = [coll.name for coll in collections]
for lcoll in list(traverse_collection_tree(context.view_layer.layer_collection)):
if lcoll.name in coll_name:
layer_colls.append(lcoll)
return layer_colls
def set_layer_coll_exlcude(
layer_collections: List[bpy.types.LayerCollection], exclude: bool
) -> List[Tuple[bpy.types.LayerCollection, bool]]:
layer_colls_vis: List[Tuple[bpy.types.LayerCollection, bool]] = []
noun = "Exclude" if exclude else "Include"
for lcoll in layer_collections:
exclude_cache = lcoll.exclude
if exclude:
if lcoll.exclude and lcoll.hide_render:
continue
lcoll.exclude = True
else:
if not lcoll.exclude:
continue
lcoll.exclude = False
layer_colls_vis.append((lcoll, exclude_cache))
if layer_colls_vis:
# Log.
logger.info(
"%s layer collections in current view layer:\n%s",
noun,
",\n".join([lcoll.name for lcoll, ex in layer_colls_vis]),
)
return layer_colls_vis
def restore_layer_coll_exlude(
lcoll_vis_list: List[Tuple[bpy.types.LayerCollection, bool]]
) -> None:
log_list: Dict[str, List[str]] = {}
for lcoll, exclude in lcoll_vis_list:
if lcoll.exclude == exclude:
continue
exclude_cache = lcoll.exclude
lcoll.exclude = exclude
# Populate log list.
_append_str_to_log_list(
log_list,
lcoll.name,
f"exclude: {exclude_cache} -> {lcoll.exclude}",
)
# Log.
_print_log_list(log_list, "Restore layer collection visibility:")
def enable_muted_drivers(
muted_drivers: List[bpy.types.Driver],
) -> List[bpy.types.Driver]:
# Log list.
log_list: Dict[str, List[str]] = {}
for driver in muted_drivers:
if driver.mute == False:
continue
driver.mute = False
# Populate log list.
_append_str_to_log_list(log_list, driver.id_data.name, driver.data_path)
# Log.
_print_log_list(log_list, "Enable drivers:")
return muted_drivers
def gen_abc_object_path(obj: bpy.types.Object) -> str:
# If object is duplicated (multiple copies of the same object that get different cachses)
# we have to kill the .001 postfix that gets created auto on duplication
# otherwise object path is not valid.
object_name = obj.name
object_path = "/" + object_name
if obj.data and obj.type != "LATTICE":
object_data_name = obj.data.name
object_path = "/" + object_name + "/" + object_data_name
# Dot and whitespace not valid in abc tree will be replaced with underscore.
replace = [" ", "."]
for char in replace:
object_path = object_path.replace(char, "_")
return str(object_path)
def disable_non_keep_modifiers(obj: bpy.types.Object) -> int:
modifiers = list(obj.modifiers)
a_index: int = -1
disabled_mods = []
for idx, mod in enumerate(modifiers):
if mod.type not in cmglobals.MODIFIERS_KEEP:
# Save index of first armature modifier to.
if a_index == -1 and mod.type == "ARMATURE":
a_index = idx
if not mod.show_viewport and not mod.show_render:
continue
mod.show_viewport = False
mod.show_render = False
mod.show_in_editmode = False
disabled_mods.append(mod.name)
if disabled_mods:
logger.info("%s Disabled modifiers: %s", obj.name, ", ".join(disabled_mods))
return a_index
def disable_non_keep_constraints(obj: bpy.types.Object) -> List[bpy.types.Constraint]:
constraints = list(obj.constraints)
disabled_const: List[bpy.types.Constraint] = []
for c in constraints:
if c.type in cmglobals.CONSTRAINTS_KEEP:
continue
if c.mute:
continue
c.mute = True
disabled_const.append(c)
if disabled_const:
logger.info(
"%s Disabled constaints: %s",
obj.name,
", ".join([c.name for c in disabled_const]),
)
return disabled_const
def ensure_cachefile(cachefile_path: str) -> bpy.types.CacheFile:
# Get cachefile path for this collection.
cachefile_name = Path(cachefile_path).name
# Import Alembic Cache. if its already imported reload it.
try:
bpy.data.cache_files[cachefile_name]
except KeyError:
bpy.ops.cachefile.open(filepath=cachefile_path)
logger.info("Imported cachefile: %s", cachefile_path)
else:
bpy.ops.cachefile.reload()
cachefile = bpy.data.cache_files[cachefile_name]
cachefile.scale = 1
return cachefile
def ensure_cache_modifier(obj: bpy.types.Object) -> bpy.types.MeshSequenceCacheModifier:
modifier_name = cmglobals.MODIFIER_NAME
# If modifier does not exist yet create it.
if obj.modifiers.find(modifier_name) == -1: # not found
mod = obj.modifiers.new(modifier_name, "MESH_SEQUENCE_CACHE")
logger.info(
"%s added %s modifier.",
obj.name,
modifier_name,
)
mod = obj.modifiers.get(modifier_name)
return mod
def ensure_cache_constraint(
obj: bpy.types.Object,
) -> bpy.types.TransformCacheConstraint:
constraint_name = cmglobals.CONSTRAINT_NAME
# If constraint does not exist yet create it.
if obj.constraints.find(constraint_name) == -1: # not found
con = obj.constraints.new("TRANSFORM_CACHE")
con.name = constraint_name
logger.info(
"%s added %s constraint.",
obj.name,
constraint_name,
)
con = obj.constraints.get(constraint_name)
return con
def kill_increment(str_value: str) -> str:
match = re.search(r"\.\d\d\d", str_value)
if match:
return str_value.replace(match.group(0), "")
return str_value
def config_cache_modifier(
context: bpy.types.Context,
mod: bpy.types.MeshSequenceCacheModifier,
modifier_index: int,
cachefile: bpy.types.CacheFile,
abc_obj_path: str,
) -> bpy.types.MeshSequenceCacheModifier:
obj = mod.id_data
# Move to index
# as we need to use bpy.ops for that object needs to be active.
bpy.context.view_layer.objects.active = obj
override = context.copy()
override["modifier"] = mod
bpy.ops.object.modifier_move_to_index(
override, modifier=mod.name, index=modifier_index
)
# Adjust settings.
mod.cache_file = cachefile
mod.object_path = abc_obj_path
return mod
def config_cache_constraint(
context: bpy.types.Context,
con: bpy.types.TransformCacheConstraint,
cachefile: bpy.types.CacheFile,
abc_obj_path: str,
) -> bpy.types.TransformCacheConstraint:
obj = con.id_data
# Move to index.
current_index = obj.constraints.find(con.name)
obj.constraints.move(current_index, 0)
# Adjust settings.
con.cache_file = cachefile
con.object_path = abc_obj_path
return con
def add_coll_to_cache_collections(
context: bpy.types.Context, coll: bpy.types.Collection, category: str
) -> Optional[bpy.types.Collection]:
scn = context.scene
scn_category = scn.cm.colls_export
idx = scn.cm.colls_export_index
if category == "IMPORT":
scn_category = scn.cm.colls_import
idx = scn.cm.colls_import_index
if coll in [c[1].coll_ptr for c in scn_category.items()]:
logger.info(
"%s already in the %s cache collections list", coll.name, category.lower()
)
# Set is_cache_coll.
coll.cm.is_cache_coll = True
return None
else:
if category == "EXPORT" and not coll.override_library and not coll.library:
# Local collection
# blend file needs to be saved for that.
if not bpy.data.filepath:
logger.error(
"Failed to add local collection %s to export list. Blend files needs to be saved.",
coll.name,
)
return None
item = scn_category.add()
item.coll_ptr = coll
item.name = item.coll_ptr.name
idx = len(scn_category) - 1
# Set is_cache_coll.
coll.cm.is_cache_coll = True
logger.info(
"%s added to %s cache collections list", item.name, category.lower()
)
return coll
def rm_coll_from_cache_collections(
context: bpy.types.Context, category: str
) -> Optional[bpy.types.Collection]:
scn = context.scene
scn_category = scn.cm.colls_export
idx = scn.cm.colls_export_index
if category == "IMPORT":
scn_category = scn.cm.colls_import
idx = scn.cm.colls_import_index
try:
item = scn_category[idx]
except IndexError:
return None
else:
item = scn_category[idx]
item_name = item.name
scn_category.remove(idx)
idx -= 1
# Reset coll.cm properties.
coll = item.coll_ptr
if coll: # check if not None (coll might be deleted)
coll.cm.reset_properties()
logger.info(
"Removed %s from %s cache collections list", item_name, category.lower()
)
return coll
def get_cache_frame_range(context: bpy.types.Context) -> Tuple[int, int]:
frame_in = context.scene.frame_start - context.scene.cm.frame_handles_left
if frame_in < 0:
frame_in = 0
frame_end = context.scene.frame_end + context.scene.cm.frame_handles_right
return (frame_in, frame_end)
def set_instancing_type_of_empties(
object_list: List[bpy.types.Object], instance_type: str
) -> List[Tuple[bpy.types.Object, str]]:
if instance_type not in cmglobals.INSTANCE_TYPES:
raise ValueError(f"Invalid instance type: {instance_type}")
empties_to_restore: List[Tuple[bpy.types.Object, str]] = []
for obj in object_list:
if not obj.type == "EMPTY":
continue
if obj.instance_type == instance_type:
continue
instance_type_cache = obj.instance_type
obj.instance_type = instance_type
empties_to_restore.append((obj, instance_type_cache))
if empties_to_restore:
logger.info(
"Set instance type to %s for empties: %s\n",
instance_type,
" ,".join([obj.name for obj, it in empties_to_restore]),
)
return empties_to_restore
def restore_instancing_type(restore_list: List[Tuple[bpy.types.Object, str]]) -> None:
# Log list.
log_list: Dict[str, List[str]] = {}
for obj, instance_type in restore_list:
if obj.instance_type == instance_type:
continue
instance_type_cache = obj.instance_type
obj.instance_type = instance_type
# Populate log list.
_append_str_to_log_list(
log_list,
obj.name,
f"{instance_type_cache}: -> {obj.instance_type}",
)
# Log.
_print_log_list(log_list, "Restore instance types:")
def is_item_local(
item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera]
) -> bool:
# Local collection of blend file.
if not item.override_library and not item.library:
return True
return False
def is_item_lib_override(
item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera]
) -> bool:
# Collection from libfile and overwritten.
if item.override_library and not item.library:
return True
return False
def is_item_lib_source(
item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera]
) -> bool:
# Source collection from libfile not overwritten.
if not item.override_library and item.library:
return True
return False
def get_item_libfile(
item: Union[bpy.types.Collection, bpy.types.Object, bpy.types.Camera]
) -> str:
if is_item_lib_source(item):
# Source collection not overwritten.
lib = item.library
return Path(os.path.abspath(bpy.path.abspath(lib.filepath))).as_posix()
if is_item_local(item):
# Local collection
# blend file needs to be saved for that.
if not bpy.data.filepath:
return ""
return Path(os.path.abspath(bpy.path.abspath(bpy.data.filepath))).as_posix()
if is_item_lib_override(item):
# Overwritten collection.
lib = item.override_library.reference.library
return Path(os.path.abspath(bpy.path.abspath(lib.filepath))).as_posix()
return ""
def set_simplify(use_simplify: bool) -> None:
if bpy.context.scene.render.use_simplify == use_simplify:
return
noun = "Enabled"
if not use_simplify:
noun = "Disabled"
bpy.context.scene.render.use_simplify = use_simplify
logger.info("%s simplify", noun)