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

282 lines
8.7 KiB
Python

# SPDX-FileCopyrightText: 2021 Blender Studio Tools Authors
#
# SPDX-License-Identifier: GPL-3.0-or-later
import re
from typing import Any, Dict, List, Optional, Tuple, Union, Generator
import bpy
from .. import bkglobals, util
from ..types import Shot
from ..logger import LoggerFactory
logger = LoggerFactory.getLogger()
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 create_collection_instance(
context: bpy.types.Context,
ref_coll: bpy.types.Collection,
instance_name: str,
) -> bpy.types.Object:
# Use empty to instance source collection.
instance_obj = bpy.data.objects.new(name=instance_name, object_data=None)
instance_obj.instance_collection = ref_coll
instance_obj.instance_type = "COLLECTION"
parent_collection = context.scene.collection
parent_collection.objects.link(instance_obj)
logger.info(
"Instanced collection: %s as: %s",
ref_coll.name,
instance_obj.name,
)
return instance_obj
def find_rig(coll: bpy.types.Collection, log: bool = True) -> Optional[bpy.types.Armature]:
valid_rigs = []
for obj in coll.all_objects:
# Default rig name: 'RIG-rex' / 'RIG-Rex'.
if obj.type != "ARMATURE":
continue
if not obj.name.startswith(bkglobals.PREFIX_RIG):
continue
valid_rigs.append(obj)
if not valid_rigs:
return None
if log:
for rig in valid_rigs:
logger.info("Found rig: %s", rig.name)
return valid_rigs
def find_asset_collections(
top_level_col: bpy.types.Collection, log: bool = True
) -> List[bpy.types.Collection]:
asset_colls: List[bpy.types.Collection] = []
for coll in top_level_col.children_recursive:
if not is_item_lib_override(coll):
continue
for prefix in bkglobals.ASSET_COLL_PREFIXES:
if not coll.name.startswith(prefix):
continue
asset_colls.append(coll)
if log:
logger.info(
"Found asset collections:\n%s", ", ".join([c.name for c in asset_colls])
)
return asset_colls
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 find_asset_collections_in_scene(
scene: bpy.types.Scene, log: bool = True
) -> List[bpy.types.Collection]:
asset_colls: List[bpy.types.Collection] = []
colls: List[bpy.types.Collection] = []
# Get all collections that are linked in this scene.
for coll in scene.collection.children:
colls.extend(list(traverse_collection_tree(coll)))
for coll in colls:
for prefix in bkglobals.ASSET_COLL_PREFIXES:
if not coll.name.startswith(prefix):
continue
asset_colls.append(coll)
if log:
logger.info(
"Found asset collections:\n%s", ", ".join([c.name for c in asset_colls])
)
return asset_colls
def get_ref_coll(coll: bpy.types.Collection) -> bpy.types.Collection:
if not coll.override_library:
return coll
return coll.override_library.reference
def find_asset_name(name: str) -> str:
name = _kill_increment_end(name)
if name.endswith(bkglobals.SPACE_REPLACER + "rig"):
name = name[:-4]
return name.split(bkglobals.DELIMITER)[-1] # CH-rex -> 'rex'
def _kill_increment_end(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 is_multi_asset(asset_name: str) -> bool:
if asset_name.startswith("thorn"):
return True
if asset_name.startswith("pine_cone"):
return True
if asset_name.startswith("pee_"):
return True
if asset_name.lower() in bkglobals.MULTI_ASSETS:
return True
return False
action_names_cache: List[str] = []
# We need this in order to increment prefixes of duplications of the same asset correctly
# gets cleared populated during call of KITSU_OT_anim_check_action_names.
_current_asset: str = ""
_current_asset_idx: int = 0
# We need these two variables to track if we are on the first asset that is currently processed
# (if there are multiple ones) because the first one CAN get keep it postfix.
def gen_action_name(
armature: bpy.types.Armature, collection: bpy.types.Collection, shot: Shot
) -> str:
global action_names_cache
global _current_asset
global _current_asset_idx
action_names_cache.sort()
def _find_postfix(action_name: str) -> Optional[str]:
# ANI-lady_bug_A.030_0020_A.v001.
split1 = action_name.split(bkglobals.DELIMITER)[-1] # lady_bug_A.030_0020_A.v001
split2 = split1.split(".")[0] # lady_bug_A
split3 = split2.split(bkglobals.SPACE_REPLACER)[-1] # A
if len(split3) == 1:
# is postfix
# print(f"{action.name} found postfix: {split3}")
return split3
else:
return None
ref_coll = get_ref_coll(collection)
action_prefix = "ANI"
asset_name = find_asset_name(ref_coll.name).lower()
asset_name = asset_name.replace(".", bkglobals.SPACE_REPLACER)
# Track on which repition we are of the same asset.
if asset_name == _current_asset:
_current_asset_idx += 1
else:
_current_asset_idx = 0
_current_asset = asset_name
version = "v001"
shot_name = shot.name
has_action = False
final_postfix = ""
# Overwrite version v001 if there is an action which already contains a version.
if armature.animation_data:
if armature.animation_data.action:
has_action = True
version = util.get_version(armature.animation_data.action.name) or "v001"
# Action name for single aset.
delimiter = bkglobals.DELIMITER # Currently set to '-'
space = bkglobals.SPACE_REPLACER # Currently set to '_'
action_name = (
f"{action_prefix}{delimiter}{asset_name}{delimiter}{shot_name}{delimiter}{version}"
)
if is_multi_asset(asset_name):
existing_postfixes = []
# Find all actions that relate to the same asset except for the asset.
for action_name in action_names_cache:
# Skip action that was input as parameter of this function.
if has_action and action_name == armature.animation_data.action.name:
# Print(f"Skipping action same name: {action_name}").
continue
# print(action_names_cache)
if action_name.startswith(f"{action_prefix}{delimiter}{asset_name}"):
multi_postfix = _find_postfix(action_name)
if multi_postfix:
# print(f"Found postfix {multi_postfix} for aseet : {asset_name}")
existing_postfixes.append(multi_postfix)
# print(f"EXISTING: {existing_postfixes}")
if existing_postfixes:
if _current_asset_idx == 0:
# print(f"{asset_name} is first asset can keep postfix")
final_postfix = multi_postfix
else:
# Otherwise increment the postfix by one.
existing_postfixes.sort()
final_postfix = chr(
ord(existing_postfixes[-1]) + 1
) # handle postfix == Z > [
else:
# If there are no existing postfixes the first one is A.
final_postfix = "A"
if has_action:
# Overwrite multi_postfix if multi_postfix exists.
current_postfix = _find_postfix(armature.animation_data.action.name)
# If existing action already has a postfix check if that one is in
# existing postfixes, if not use the actions post fix.
if current_postfix:
if current_postfix not in existing_postfixes:
final_postfix = current_postfix
# Action name for multi asset.
action_name = f"{action_prefix}{delimiter}{asset_name}{space}{final_postfix}{delimiter}{shot_name}{delimiter}{version}"
return action_name