Merge branch 'main' into pr/82

This commit is contained in:
Yusarina
2024-12-16 13:09:39 +00:00
70 changed files with 7070 additions and 4693 deletions
-20
View File
@@ -1,20 +0,0 @@
# core/__init__.py
from .register import register_wrap
#to reload all things in this directory and import them properly - @989onan
if "bpy" not in locals():
import bpy
import glob
import os
from os.path import dirname, basename, isfile, join
modules = glob.glob(join(dirname(__file__), "*.py"))
for module_name in [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]:
exec("from . import "+module_name)
print("importing " +module_name)
else:
import importlib
modules = glob.glob(join(dirname(__file__), "*.py"))
for module_name in [ basename(f)[:-3] for f in modules if isfile(f) and not f.endswith('__init__.py')]:
exec("importlib.reload("+module_name+")")
print("reloading " +module_name)
+16 -5
View File
@@ -2,6 +2,7 @@ import bpy
import os
import tomllib
import json
from ..core.logging_setup import logger
from bpy.types import AddonPreferences
from typing import Any, Dict
@@ -12,22 +13,31 @@ PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json")
def get_current_version():
main_dir = os.path.dirname(os.path.dirname(__file__))
manifest_path = os.path.join(main_dir, "blender_manifest.toml")
logger.debug(f"Reading version from manifest: {manifest_path}")
with open(manifest_path, 'rb') as f:
manifest_data = tomllib.load(f)
return manifest_data.get('version', 'Unknown')
version = manifest_data.get('version', 'Unknown')
logger.info(f"Current addon version: {version}")
return version
def save_preference(key: str, value: Any) -> None:
"""Save a single preference to the JSON file."""
logger.debug(f"Saving preference: {key} = {value}")
prefs = load_preferences()
prefs[key] = value
with open(PREFERENCES_FILE, 'w') as f:
json.dump(prefs, f, indent=4)
logger.info(f"Preference saved: {key}")
def load_preferences() -> Dict[str, Any]:
"""Load all preferences from the JSON file."""
logger.debug(f"Loading preferences from: {PREFERENCES_FILE}")
if os.path.exists(PREFERENCES_FILE):
with open(PREFERENCES_FILE, 'r') as f:
return json.load(f)
prefs = json.load(f)
logger.debug(f"Loaded preferences: {prefs}")
return prefs
logger.info("No preferences file found, using defaults")
return {}
def get_preference(key: str, default: Any = None) -> Any:
@@ -40,12 +50,13 @@ class AvatarToolkitPreferences(AddonPreferences):
def draw(self, context):
layout = self.layout
layout.label(text="Preferences are managed internally.")
# You can add more UI elements here if needed
layout.label(text=f"Version: {get_current_version()}")
def get_addon_preferences(context):
return context.preferences.addons[AvatarToolkitPreferences.bl_idname].preferences
# Initialize preferences if the file doesn't exist
if not os.path.exists(PREFERENCES_FILE):
save_preference("language", 0) # Set default language to 0 (auto)
save_preference("language", 0) # Set default language to 0 (auto)
save_preference("validation_mode", "STRICT") # Set default validation mode
save_preference("enable_logging", False) # Set default logging mode
+193
View File
@@ -0,0 +1,193 @@
import os
import bpy
import sys
import typing
import inspect
import pkgutil
import tomllib
import importlib
from pathlib import Path
from typing import List, Dict, Set, Optional, Any, Type, Tuple, Generator, TypeVar
__all__ = (
"init",
"register",
"unregister",
)
T = TypeVar('T')
modules: Optional[List[Any]] = None
ordered_classes: Optional[List[Type]] = None
def init() -> None:
"""Initialize the auto-loader by discovering modules and classes"""
global modules
global ordered_classes
# Configure logging first
from .logging_setup import configure_logging
configure_logging(False)
from .addon_preferences import get_preference
configure_logging(get_preference("enable_logging", False))
print("Auto-load init starting")
modules = get_all_submodules(Path(__file__).parent.parent)
ordered_classes = get_ordered_classes_to_register(modules)
print(f"Found modules: {modules}")
print(f"Found classes: {ordered_classes}")
def register() -> None:
"""Register all discovered classes and modules"""
print("Registering classes")
for cls in ordered_classes:
print(f"Registering: {cls}")
try:
bpy.utils.register_class(cls)
except ValueError:
continue
for module in modules:
if module.__name__ == __name__:
continue
if hasattr(module, "register"):
module.register()
def unregister() -> None:
"""Unregister all classes and modules in reverse order"""
for cls in reversed(ordered_classes):
bpy.utils.unregister_class(cls)
for module in modules:
if module.__name__ == __name__:
continue
if hasattr(module, "unregister"):
module.unregister()
def get_manifest_id() -> str:
"""Get the addon ID from the manifest file"""
manifest_path = Path(__file__).parent.parent / "blender_manifest.toml"
with open(manifest_path, "rb") as f:
manifest = tomllib.load(f)
return manifest["id"]
def get_all_submodules(directory: Path) -> List[Any]:
"""Discover and import all submodules in the given directory"""
modules = []
addon_id = get_manifest_id()
for root, dirs, files in os.walk(directory):
if "__pycache__" in root:
continue
path = Path(root)
if path == directory:
package_name = f"bl_ext.user_default.{addon_id}"
else:
relative_path = path.relative_to(directory).as_posix().replace('/', '.')
package_name = f"bl_ext.user_default.{addon_id}.{relative_path}"
for name in sorted(iter_module_names(path)):
modules.append(importlib.import_module(f".{name}", package_name))
return modules
def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]:
"""Iterate through submodules in a package"""
for name in sorted(iter_module_names(path)):
yield importlib.import_module("." + name, package_name)
def iter_module_names(path: Path) -> Generator[str, None, None]:
"""Iterate through module names in a directory"""
print(f"Scanning path: {path}")
modules_list = list(pkgutil.iter_modules([str(path)]))
print(f"Found these modules: {modules_list}")
for _, module_name, is_pkg in modules_list:
if not is_pkg:
print(f"Found module: {module_name}")
yield module_name
def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]:
"""Get a topologically sorted list of classes to register"""
return toposort(get_register_deps_dict(modules))
def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]:
"""Get dependencies dictionary for class registration"""
deps_dict = {}
classes_to_register = set(iter_classes_to_register(modules))
for cls in classes_to_register:
deps_dict[cls] = set(iter_own_register_deps(cls, classes_to_register))
return deps_dict
def iter_own_register_deps(cls: Type, classes_to_register: Set[Type]) -> Generator[Type, None, None]:
"""Iterate through a class's own registration dependencies"""
yield from (dep for dep in iter_register_deps(cls) if dep in classes_to_register)
def iter_register_deps(cls: Type) -> Generator[Type, None, None]:
"""Iterate through all registration dependencies of a class"""
for value in typing.get_type_hints(cls, {}, {}).values():
dependency = get_dependency_from_annotation(value)
if dependency is not None:
yield dependency
def get_dependency_from_annotation(value: Any) -> Optional[Type]:
"""Get dependency type from a type annotation"""
if isinstance(value, tuple) and len(value) == 2:
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
return value[1]["type"]
return None
def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]:
"""Iterate through classes that need to be registered"""
base_types = get_register_base_types()
for cls in get_classes_in_modules(modules):
if any(base in base_types for base in cls.__bases__):
if not getattr(cls, "_is_registered", False):
yield cls
def get_classes_in_modules(modules: List[Any]) -> Set[Type]:
"""Get all classes defined in the modules"""
classes = set()
for module in modules:
for cls in iter_classes_in_module(module):
classes.add(cls)
return classes
def iter_classes_in_module(module: Any) -> Generator[Type, None, None]:
"""Iterate through classes defined in a module"""
for value in module.__dict__.values():
if inspect.isclass(value):
yield value
def get_register_base_types() -> Set[Type]:
"""Get set of base types that need registration"""
return set(getattr(bpy.types, name) for name in [
"Panel", "Operator", "PropertyGroup",
"AddonPreferences", "Header", "Menu",
"Node", "NodeSocket", "NodeTree",
"UIList", "RenderEngine"
])
def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]:
"""Topologically sort classes based on their dependencies"""
sorted_list = []
sorted_values = set()
panels_to_sort = [(value, deps) for value, deps in deps_dict.items()
if hasattr(value, 'bl_parent_id')]
base_panels = [(value, deps) for value, deps in deps_dict.items()
if not hasattr(value, 'bl_parent_id')]
for value, deps in base_panels:
if len(deps) == 0:
sorted_list.append(value)
sorted_values.add(value)
while len(deps_dict) > len(sorted_values):
unsorted = []
for value, deps in deps_dict.items():
if value not in sorted_values:
if len(deps - sorted_values) == 0:
sorted_list.append(value)
sorted_values.add(value)
else:
unsorted.append(value)
return sorted_list
+532 -404
View File
File diff suppressed because it is too large Load Diff
+324 -80
View File
@@ -5,89 +5,277 @@
# Note from @989onan: Please make sure to make your names are lowercase in this array. I banged my head metaphorically till I figured that out...
# Taken from Tuxedo/Cats
bone_names = {
"right_shoulder": ["rightshoulder", "shoulderr", "rshoulder", "valvebipedbip01rclavicle"],
"right_arm": ["rightarm", "armr", "rarm", "upperarmr", "rupperarm", "rightupperarm", "uparmr", "ruparm", "valvebipedbip01rupperarm"],
"right_elbow": ["rightelbow", "elbowr", "relbow", "lowerarmr", "rightlowerarm", "lowerarmr","rlowerarm", "lowarmr", "rlowarm", "forearmr","rforearm", "valvebipedbip01rforearm"],
"right_wrist": ["rightwrist", "wristr", "rwrist", "handr", "righthand", "rhand", "valvebipedbip01rhand"],
# Right side bones
"right_shoulder": [
"rightshoulder", "shoulderr", "rshoulder", "valvebipedbip01rclavicle",
"右肩", "肩.r", "肩+.r", "右肩+", "右肩", "右肩+", "肩+r", "肩+右", "ik_肩.r"
],
"right_arm": [
"rightarm", "armr", "rarm", "upperarmr", "rupperarm", "rightupperarm",
"uparmr", "ruparm", "valvebipedbip01rupperarm", "右腕", "腕.r", "右腕", "ik_腕.r"
],
"right_elbow": [
"rightelbow", "elbowr", "relbow", "lowerarmr", "rightlowerarm",
"rlowerarm", "lowarmr", "rlowarm", "forearmr", "rforearm",
"valvebipedbip01rforearm", "右ひじ", "ひじ.r", "ik_ひじ.r"
],
"right_wrist": [
"rightwrist", "wristr", "rwrist", "handr", "righthand", "rhand",
"valvebipedbip01rhand", "右手首", "手首.r", "ik_手首.r"
],
"pinkie_0_r": [
"littlefinger0r", "pinkie0r", "rpinkie0", "pinkiemetacarpalr", "右小指0"
],
"pinkie_1_r": [
"littlefinger1r", "pinkie1r", "rpinkie1", "pinkieproximalr",
"valvebipedbip01rfinger4", "右小指1"
],
"pinkie_2_r": [
"littlefinger2r", "pinkie2r", "rpinkie2", "pinkieintermediater",
"valvebipedbip01rfinger41", "右小指2"
],
"pinkie_3_r": [
"littlefinger3r", "pinkie3r", "rpinkie3", "pinkiedistalr",
"valvebipedbip01rfinger42", "右小指3"
],
"ring_0_r": [
"ringfinger0r", "ring0r", "rring0", "ringmetacarpalr", "右薬指0"
],
"ring_1_r": [
"ringfinger1r", "ring1r", "rring1", "ringproximalr",
"valvebipedbip01rfinger3", "右薬指1"
],
"ring_2_r": [
"ringfinger2r", "ring2r", "rring2", "ringintermediater",
"valvebipedbip01rfinger31", "右薬指2"
],
"ring_3_r": [
"ringfinger3r", "ring3r", "rring3", "ringdistalr",
"valvebipedbip01rfinger32", "右薬指3"
],
"middle_0_r": [
"middlefinger0r", "middle0r", "rmiddle0", "middlemetacarpalr", "右中指0"
],
"middle_1_r": [
"middlefinger1r", "middle1r", "rmiddle1", "middleproximalr",
"valvebipedbip01rfinger2", "右中指1"
],
"middle_2_r": [
"middlefinger2r", "middle2r", "rmiddle2", "middleintermediater",
"valvebipedbip01rfinger21", "右中指2"
],
"middle_3_r": [
"middlefinger3r", "middle3r", "rmiddle3", "middledistalr",
"valvebipedbip01rfinger22", "右中指3"
],
"index_0_r": [
"indexfinger0r", "index0r", "rindex0", "indexmetacarpalr", "右人差指0"
],
"index_1_r": [
"indexfinger1r", "index1r", "rindex1", "indexproximalr",
"valvebipedbip01rfinger1", "右人差指1"
],
"index_2_r": [
"indexfinger2r", "index2r", "rindex2", "indexintermediater",
"valvebipedbip01rfinger11", "右人差指2"
],
"index_3_r": [
"indexfinger3r", "index3r", "rindex3", "indexdistalr",
"valvebipedbip01rfinger12", "右人差指3"
],
"thumb_0_r": [
"thumb0r", "rthumb0", "thumbmetacarpalr", "右親指0"
],
"thumb_1_r": [
"thumb1r", "rthumb1", "thumbproximalr", "valvebipedbip01rfinger0", "右親指1"
],
"thumb_2_r": [
"thumb2r", "rthumb2", "thumbintermediater", "valvebipedbip01rfinger01", "右親指2"
],
"thumb_3_r": [
"thumb3r", "rthumb3", "thumbdistalr", "valvebipedbip01rfinger02", "右親指3"
],
"right_leg": [
"rightleg", "legr", "rleg", "upperlegr", "rupperleg", "thighr",
"rightupperleg", "uplegr", "rupleg", "valvebipedbip01rthigh",
"右足", "足.r", "ik_足.r"
],
"right_knee": [
"rightknee", "kneer", "rknee", "lowerlegr", "rightlowerleg",
"rlowerleg", "lowlegr", "rlowleg", "calfr", "rcalf",
"valvebipedbip01rcalf", "右ひざ", "ひざ.r", "すね.r", "ik_ひざ.r"
],
"right_ankle": [
"rightankle", "ankler", "rankle", "rightfoot", "footr", "rfoot",
"rightfeet", "feetright", "rfeet", "feetr", "valvebipedbip01rfoot",
"右足首", "足首.r", "ik_足首.r"
],
"right_toe": [
"righttoe", "toeright", "toer", "rtoe", "toesr", "rtoes",
"valvebipedbip01rtoe0", "右つま先", "つま先.r", "ik_つま先.r"
],
#hand l fingers
"pinkie_0_r": ["littlefinger0r","pinkie0r","rpinkie0","pinkiemetacarpalr"],
"pinkie_1_r": ["littlefinger1r","pinkie1r","rpinkie1","pinkieproximalr", "valvebipedbip01rfinger4"],
"pinkie_2_r": ["littlefinger2r","pinkie2r","rpinkie2","pinkieintermediater", "valvebipedbip01rfinger41"],
"pinkie_3_r": ["littlefinger3r","pinkie3r","rpinkie3","pinkiedistalr", "valvebipedbip01rfinger42"],
# Left side bones
"left_shoulder": [
"leftshoulder", "shoulderl", "lshoulder", "valvebipedbip01lclavicle",
"左肩", "肩.l", "肩+.l", "左肩+", "左肩", "左肩+", "肩+l", "肩+左", "ik_肩.l"
],
"left_arm": [
"leftarm", "arml", "larm", "upperarml", "lupperarm", "leftupperarm",
"uparml", "luparm", "valvebipedbip01lupperarm", "左腕", "腕.l", "左腕", "ik_腕.l"
],
"left_elbow": [
"leftelbow", "elbowl", "lelbow", "lowerarml", "leftlowerarm",
"llowerarm", "lowarml", "llowarm", "forearml", "lforearm",
"valvebipedbip01lforearm", "左ひじ", "ひじ.l", "すね.l", "ik_ひじ.l"
],
"left_wrist": [
"leftwrist", "wristl", "lwrist", "handl", "lefthand", "lhand",
"valvebipedbip01lhand", "左手首", "手首.l", "ik_手首.l"
],
"pinkie_0_l": [
"pinkiefinger0l", "pinkie0l", "lpinkie0", "pinkiemetacarpall", "左小指0"
],
"pinkie_1_l": [
"littlefinger1l", "pinkie1l", "lpinkie1", "pinkieproximall",
"valvebipedbip01lfinger4", "左小指1"
],
"pinkie_2_l": [
"littlefinger2l", "pinkie2l", "lpinkie2", "pinkieintermediatel",
"valvebipedbip01lfinger41", "左小指2"
],
"pinkie_3_l": [
"littlefinger3l", "pinkie3l", "lpinkie3", "pinkiedistall",
"valvebipedbip01lfinger42", "左小指3"
],
"ring_0_l": [
"ringfinger0l", "ring0l", "lring0", "ringmetacarpall", "左薬指0"
],
"ring_1_l": [
"ringfinger1l", "ring1l", "lring1", "ringproximall",
"valvebipedbip01lfinger3", "左薬指1"
],
"ring_2_l": [
"ringfinger2l", "ring2l", "lring2", "ringintermediatel",
"valvebipedbip01lfinger31", "左薬指2"
],
"ring_3_l": [
"ringfinger3l", "ring3l", "lring3", "ringdistall",
"valvebipedbip01lfinger32", "左薬指3"
],
"middle_0_l": [
"middlefinger0l", "middle_0l", "lmiddle0", "middlemetacarpall", "左中指0"
],
"middle_1_l": [
"middlefinger1l", "middle_1l", "lmiddle1", "middleproximall",
"valvebipedbip01lfinger2", "左中指1"
],
"middle_2_l": [
"middlefinger2l", "middle_2l", "lmiddle2", "middleintermediatel",
"valvebipedbip01lfinger21", "左中指2"
],
"middle_3_l": [
"middlefinger3l", "middle_3l", "lmiddle3", "middledistall",
"valvebipedbip01lfinger22", "左中指3"
],
"index_0_l": [
"indexfinger0l", "index0l", "lindex0", "indexmetacarpall", "左人差指0"
],
"index_1_l": [
"indexfinger1l", "index1l", "lindex1", "indexproximall",
"valvebipedbip01lfinger1", "左人差指1"
],
"index_2_l": [
"indexfinger2l", "index2l", "lindex2", "indexintermediatel",
"valvebipedbip01lfinger11", "左人差指2"
],
"index_3_l": [
"indexfinger3l", "index3l", "lindex3", "indexdistall",
"valvebipedbip01lfinger12", "左人差指3"
],
"thumb_0_l": [
"thumb0l", "lthumb0", "thumbmetacarpall", "左親指0"
],
"thumb_1_l": [
"thumb1l", "lthumb1", "thumbproximall", "valvebipedbip01lfinger0", "左親指1"
],
"thumb_2_l": [
"thumb2l", "lthumb2", "thumbintermediatel", "valvebipedbip01lfinger01", "左親指2"
],
"thumb_3_l": [
"thumb3l", "lthumb3", "thumbdistall", "valvebipedbip01lfinger02", "左親指3"
],
"left_leg": [
"leftleg", "legl", "lleg", "upperlegl", "lupperleg", "thighl",
"leftupperleg", "uplegl", "lupleg", "valvebipedbip01lthigh",
"左足", "足.l", "ik_足.l"
],
"left_knee": [
"leftknee", "kneel", "lknee", "lowerlegl", "leftlowerleg",
"llowerleg", "lowlegl", "llowleg", "calfl", "lcalf",
"valvebipedbip01lcalf", "左ひざ", "ひざ.l", "すね.l", "ik_ひざ.l"
],
"left_ankle": [
"leftankle", "anklel", "lankle", "leftfoot", "footl", "lfoot",
"leftfeet", "feetleft", "lfeet", "feetl", "valvebipedbip01lfoot",
"左足首", "足首.l", "ik_足首.l"
],
"left_toe": [
"lefttoe", "toeleft", "toel", "ltoe", "toesl", "ltoes",
"valvebipedbip01ltoe0", "左つま先", "つま先.l", "ik_つま先.l"
],
"ring_0_r": ["ringfinger0r","ring0r","rring0","ringmetacarpalr"],
"ring_1_r": ["ringfinger1r","ring1r","rring1","ringproximalr", "valvebipedbip01rfinger3"],
"ring_2_r": ["ringfinger2r","ring2r","rring2","ringintermediater", "valvebipedbip01rfinger31"],
"ring_3_r": ["ringfinger3r","ring3r","rring3","ringdistalr", "valvebipedbip01rfinger32"],
"middle_0_r": ["middlefinger0r","middle0r","rmiddle0","middlemetacarpalr"],
"middle_1_r": ["middlefinger1r","middle1r","rmiddle1","middleproximalr", "valvebipedbip01rfinger2"],
"middle_2_r": ["middlefinger2r","middle2r","rmiddle2","middleintermediater", "valvebipedbip01rfinger21"],
"middle_3_r": ["middlefinger3r","middle3r","rmiddle3","middledistalr", "valvebipedbip01rfinger22"],
"index_0_r": ["indexfinger0r","index0r","rindex0","indexmetacarpalr"],
"index_1_r": ["indexfinger1r","index1r","rindex1","indexproximalr", "valvebipedbip01rfinger1"],
"index_2_r": ["indexfinger2r","index2r","rindex2","indexintermediater", "valvebipedbip01rfinger11"],
"index_3_r": ["indexfinger3r","index3r","rindex3","indexdistalr", "valvebipedbip01rfinger12"],
"thumb_0_r": ["thumb0r","rthumb0","thumbmetacarpalr"],
"thumb_1_r": ['thumb1r',"rthumb1","thumbproximalr", "valvebipedbip01rfinger0"],
"thumb_2_r": ['thumb2r',"rthumb2","thumbintermediater", "valvebipedbip01rfinger01"],
"thumb_3_r": ['thumb3r',"rthumb3","thumbdistalr", "valvebipedbip01rfinger02"],
"right_leg": ["rightleg", "legr", "rleg", "upperlegr", "rupperleg", "thighr", "rightupperleg", "uplegr", "rupleg", "valvebipedbip01rthigh"],
"right_knee": ["rightknee", "kneer", "rknee", "lowerlegr", "calfr", "rlowerleg", "rcalf", "rightlowerleg", "lowlegr", "rlowleg", "valvebipedbip01rcalf"],
"right_ankle": ["rightankle", "ankler", "rankle", "rightfoot", "footr", "rfoot", "rightfoot", "rightfeet", "feetright", "rfeet", "feetr", "valvebipedbip01rfoot"],
"right_toe": ["righttoe", "toeright", "toer", "rtoe", "toesr", "rtoes", "valvebipedbip01rtoe0"],
"left_shoulder": ["leftshoulder", "shoulderl", "lshoulder", "valvebipedbip01lclavicle"],
"left_arm": ["leftarm", "arml", "rarm", "upperarml", "lupperarm", "leftupperarm", "uparml", "luparm", "valvebipedbip01lupperarm"],
"left_elbow": ["leftelbow", "elbowl", "lelbow", "lowerarml", "leftlowerarm", "lowerarml", "llowerarm", "lowarml", "llowarm", "forearml","lforearm", "valvebipedbip01lforearm"],
"left_wrist": ["leftwrist", "wristl", "lwrist", "handl", "lefthand", "lhand", "valvebipedbip01lhand"],
#hand l fingers
"pinkie_0_l": ["pinkiefinger0l","pinkie0l","lpinkie0","pinkiemetacarpall"],
"pinkie_1_l": ["littlefinger1l","pinkie1l","lpinkie1","pinkieproximall", "valvebipedbip01lfinger4"],
"pinkie_2_l": ["littlefinger2l","pinkie2l","lpinkie2","pinkieintermediatel", "valvebipedbip01lfinger41"],
"pinkie_3_l": ["littlefinger3l","pinkie3l","lpinkie3","pinkiedistall", "valvebipedbip01lfinger42"],
"ring_0_l": ["ringfinger0l","ring0l","lring0","ringmetacarpall"],
"ring_1_l": ["ringfinger1l","ring1l","lring1","ringproximall", "valvebipedbip01lfinger3"],
"ring_2_l": ["ringfinger2l","ring2l","lring2","ringintermediatel", "valvebipedbip01lfinger31"],
"ring_3_l": ["ringfinger3l","ring3l","lring3","ringdistall", "valvebipedbip01lfinger32"],
"middle_0_l": ["middlefinger0l","middle_0l","lmiddle0","middlemetacarpall"],
"middle_1_l": ["middlefinger1l","middle_1l","lmiddle1","middleproximall", "valvebipedbip01lfinger2"],
"middle_2_l": ["middlefinger2l","middle_2l","lmiddle2","middleintermediatel", "valvebipedbip01lfinger21"],
"middle_3_l": ["middlefinger3l","middle_3l","lmiddle3","middledistall", "valvebipedbip01lfinger22"],
"index_0_l": ["indexfinger0l","index0l","lindex0","indexmetacarpall"],
"index_1_l": ["indexfinger1l","index1l","lindex1","indexproximall", "valvebipedbip01lfinger1"],
"index_2_l": ["indexfinger2l","index2l","lindex2","indexintermediatel", "valvebipedbip01lfinger11"],
"index_3_l": ["indexfinger3l","index3l","lindex3","indexdistall", "valvebipedbip01lfinger12"],
"thumb_0_l": ["thumb0l","lthumb0","thumbmetacarpall"],
"thumb_1_l": ['thumb1l',"lthumb1","thumbproximall", "valvebipedbip01lfinger0"],
"thumb_2_l": ['thumb2l',"lthumb2","thumbintermediatel", "valvebipedbip01lfinger01"],
"thumb_3_l": ['thumb3l',"lthumb3","thumbdistall", "valvebipedbip01lfinger02"],
"left_leg": ["leftleg", "legl", "lleg", "upperlegl", "lupperleg", "thighl", "leftupperleg", "uplegl", "lupleg", "valvebipedbip01lthigh"],
"left_knee": ["leftknee", "kneel", "lknee", "lowerlegl", "llowerleg", "calfl", "lcalf", "leftlowerleg", 'lowlegl', 'llowleg', "valvebipedbip01lcalf"],
"left_ankle": ["leftankle", "anklel", "rankle", "leftfoot", "footl", "lfoot", "leftfoot", "leftfeet", "feetleft", "lfeet", "feetl", "valvebipedbip01lfoot"],
"left_toe": ["lefttoe", "toeleft", "toel", "ltoe", "toesl", "ltoes", "valvebipedbip01ltoe0"],
"hips": ["pelvis", "hips", "hip", "valvebipedbip01pelvis"],
"spine": ["torso", "spine", "valvebipedbip01spine"],
"chest": ["chest", "valvebipedbip01spine1"],
"upper_chest": ["upperchest", "valvebipedbip01spine4"],
"neck": ["neck", "valvebipedbip01neck1"],
"head": ["head", "valvebipedbip01head1"],
"left_eye": ["eyeleft", "lefteye", "eyel", "leye"],
"right_eye": ["eyeright", "righteye", "eyer", "reye"],
# Central bones
"hips": [
"pelvis", "hips", "hip", "valvebipedbip01pelvis", "", "ik_腰"
],
"spine": [
"torso", "spine", "valvebipedbip01spine", "脊椎", "ik_脊椎"
],
"chest": [
"chest", "valvebipedbip01spine1", "", "ik_胸"
],
"upper_chest": [
"upperchest", "valvebipedbip01spine4", "上胸", "ik_上胸"
],
"neck": [
"neck", "valvebipedbip01neck1", "", "ik_首"
],
"head": [
"head", "valvebipedbip01head1", "", "ik_頭"
],
"left_eye": [
"eyeleft", "lefteye", "eyel", "leye", "左目", "ik_左目"
],
"right_eye": [
"eyeright", "righteye", "eyer", "reye", "右目", "ik_右目"
],
}
# Add VRM bone name variations
bone_names.update({
'hips': bone_names['hips'] + ['j_bip_c_hips', 'j_hips', 'vrm_hips'],
'spine': bone_names['spine'] + ['j_bip_c_spine', 'j_spine', 'vrm_spine'],
'chest': bone_names['chest'] + ['j_bip_c_chest', 'j_chest', 'vrm_chest'],
'upper_chest': bone_names['upper_chest'] + ['j_bip_c_upper_chest', 'j_upper_chest', 'vrm_upperchest'],
'neck': bone_names['neck'] + ['j_bip_c_neck', 'j_neck', 'vrm_neck'],
'head': bone_names['head'] + ['j_bip_c_head', 'j_head', 'vrm_head'],
# VRM specific finger naming
'thumb_0_l': bone_names['thumb_0_l'] + ['thumb_metacarpal_l', 'j_thumb1_l'],
'index_0_l': bone_names['index_0_l'] + ['index_metacarpal_l', 'j_index1_l'],
'middle_0_l': bone_names['middle_0_l'] + ['middle_metacarpal_l', 'j_middle1_l'],
'ring_0_l': bone_names['ring_0_l'] + ['ring_metacarpal_l', 'j_ring1_l'],
'pinkie_0_l': bone_names['pinkie_0_l'] + ['little_metacarpal_l', 'j_little1_l'],
# Mirror for right side
'thumb_0_r': bone_names['thumb_0_r'] + ['thumb_metacarpal_r', 'j_thumb1_r'],
'index_0_r': bone_names['index_0_r'] + ['index_metacarpal_r', 'j_index1_r'],
'middle_0_r': bone_names['middle_0_r'] + ['middle_metacarpal_r', 'j_middle1_r'],
'ring_0_r': bone_names['ring_0_r'] + ['ring_metacarpal_r', 'j_ring1_r'],
'pinkie_0_r': bone_names['pinkie_0_r'] + ['little_metacarpal_r', 'j_little1_r']
})
# array taken from cats
dont_delete_these_main_bones = [
'Hips', 'Spine', 'Chest', 'Upper Chest', 'Neck', 'Head',
@@ -109,4 +297,60 @@ dont_delete_these_main_bones = [
'MiddleFinger1_R', 'MiddleFinger2_R', 'MiddleFinger3_R',
'RingFinger1_R', 'RingFinger2_R', 'RingFinger3_R',
'LittleFinger1_R', 'LittleFinger2_R', 'LittleFinger3_R',
]
]
resonite_translations = {
'hips': "Hips",
'spine': "Spine",
'chest': "Chest",
'neck': "Neck",
'head': "Head",
'left_eye': "Eye.L",
'right_eye': "Eye.R",
'right_leg': "UpperLeg.R",
'right_knee': "Calf.R",
'right_ankle': "Foot.R",
'right_toe': 'Toes.R',
'right_shoulder': "Shoulder.R",
'right_arm': "UpperArm.R",
'right_elbow': "ForeArm.R",
'right_wrist': "Hand.R",
'left_leg': "UpperLeg.L",
'left_knee': "Calf.L",
'left_ankle': "Foot.L",
'left_toe': "Toes.L",
'left_shoulder': "Shoulder.L",
'left_arm': "UpperArm.L",
'left_elbow': "ForeArm.L",
'left_wrist': "Hand.L",
'pinkie_1_l': "pinkie1.L",
'pinkie_2_l': "pinkie2.L",
'pinkie_3_l': "pinkie3.L",
'ring_1_l': "ring1.L",
'ring_2_l': "ring2.L",
'ring_3_l': "ring3.L",
'middle_1_l': "middle1.L",
'middle_2_l': "middle2.L",
'middle_3_l': "middle3.L",
'index_1_l': "index1.L",
'index_2_l': "index2.L",
'index_3_l': "index3.L",
'thumb_1_l': "thumb1.L",
'thumb_2_l': "thumb2.L",
'thumb_3_l': "thumb3.L",
'pinkie_1_r': "pinkie1.R",
'pinkie_2_r': "pinkie2.R",
'pinkie_3_r': "pinkie3.R",
'ring_1_r': "ring1.R",
'ring_2_r': "ring2.R",
'ring_3_r': "ring3.R",
'middle_1_r': "middle1.R",
'middle_2_r': "middle2.R",
'middle_3_r': "middle3.R",
'index_1_r': "index1.R",
'index_2_r': "index2.R",
'index_3_r': "index3.R",
'thumb_1_r': "thumb1.R",
'thumb_2_r': "thumb2.R",
'thumb_3_r': "thumb3.R"
}
View File
View File
+129
View File
@@ -0,0 +1,129 @@
import bpy
import logging
import os
import typing
from typing import Optional, Callable, Dict, List, Union, Set
from ..common import clear_default_objects
from .import_pmx import import_pmx
from .import_pmd import import_pmd
# Configure logging
logging.basicConfig(level=logging.INFO)
logger: logging.Logger = logging.getLogger(__name__)
import importlib.util
if importlib.util.find_spec("io_scene_valvesource") is not None:
from io_scene_valvesource.import_smd import SmdImporter
class ImportProgress:
"""Tracks and logs the progress of multi-file imports"""
def __init__(self, total_files: int):
self.total: int = total_files
self.current: int = 0
def update(self, filename: str) -> None:
"""Update import progress and log current file"""
self.current += 1
logger.info(f"Importing {filename} ({self.current}/{self.total})")
def validate_file(filepath: str) -> bool:
"""
Validate if a file exists and is accessible
Returns: True if file is valid, False otherwise
"""
if not os.path.exists(filepath):
logger.error(f"File not found: {filepath}")
return False
if not os.path.isfile(filepath):
logger.error(f"Not a file: {filepath}")
return False
return True
def import_multi_files(
method: Optional[Callable] = None,
directory: Optional[str] = None,
files: Optional[List[Dict[str, str]]] = None,
filepath: str = "",
progress_callback: Optional[Callable[[str], None]] = None
) -> None:
"""
Import multiple files using the specified import method
Args:
method: Import method to use
directory: Directory containing files
files: List of files to import
filepath: Single file path to import
progress_callback: Callback for progress updates
"""
try:
if not method:
raise ValueError("Import method not specified")
if not files:
if not validate_file(filepath):
return
method(directory, filepath)
if progress_callback:
progress_callback(filepath)
else:
progress = ImportProgress(len(files))
for file in files:
fullpath: str = os.path.join(directory, os.path.basename(file["name"]))
if not validate_file(fullpath):
continue
logger.info(f"Importing file: {fullpath}")
method(directory, fullpath)
if progress_callback:
progress_callback(fullpath)
progress.update(file["name"])
except Exception as e:
logger.error(f"Import failed: {str(e)}", exc_info=True)
raise
ImportMethod = Callable[[str, List[Dict[str, str]], str], None]
import_types: Dict[str, ImportMethod] = {
"fbx": lambda directory, files, filepath: bpy.ops.import_scene.fbx(
files=files, directory=directory, filepath=filepath,
automatic_bone_orientation=False, use_prepost_rot=False, use_anim=False
),
"smd": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
"dmx": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
"gltf": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath),
"glb": lambda directory, files, filepath: bpy.ops.import_scene.gltf(files=files, filepath=filepath),
"qc": lambda directory, files, filepath: eval("bpy."+SmdImporter.bl_idname+".(files=files, directory=directory, filepath=filepath)"),
"obj": lambda directory, files, filepath: bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath),
"dae": lambda directory, files, filepath: import_multi_files(
directory=directory,
files=files,
filepath=filepath,
method=lambda directory, filepath: bpy.ops.wm.collada_import(
filepath=filepath, auto_connect=True, find_chains=True, fix_orientation=True
)
),
"3ds": lambda directory, files, filepath: bpy.ops.import_scene.max3ds(files=files, directory=directory, filepath=filepath),
"stl": lambda directory, files, filepath: bpy.ops.import_mesh.stl(files=files, directory=directory, filepath=filepath),
"mtl": lambda directory, files, filepath: bpy.ops.wm.obj_import(files=files, directory=directory, filepath=filepath),
"x3d": lambda directory, files, filepath: bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath),
"wrl": lambda directory, files, filepath: bpy.ops.import_scene.x3d(files=files, directory=directory, filepath=filepath),
"vmd": lambda directory, files, filepath: import_multi_files(
directory=directory,
files=files,
filepath=filepath,
method=lambda directory, filepath: bpy.ops.tuxedo.import_mmd_animation(directory=directory, filepath=filepath)
),
"vrm": lambda directory, files, filepath: bpy.ops.import_scene.vrm(filepath=filepath),
"pmx": lambda directory, files, filepath: import_pmx(filepath),
"pmd": lambda directory, files, filepath: import_pmd(filepath),
}
def concat_imports_filter(imports: Dict[str, ImportMethod]) -> str:
"""Create a file filter string from import types"""
return "".join(f"*.{importer};" for importer in imports.keys())
imports: str = concat_imports_filter(import_types)
+26
View File
@@ -0,0 +1,26 @@
import logging
from typing import Optional
logger = logging.getLogger('avatar_toolkit')
def configure_logging(enabled: bool = False) -> None:
"""Configure logging for Avatar Toolkit"""
logger.setLevel(logging.DEBUG if enabled else logging.WARNING)
# Remove existing handlers
for handler in logger.handlers[:]:
logger.removeHandler(handler)
if enabled:
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
def update_logging_state(self, context) -> None:
"""Update logging state based on user preference"""
from .addon_preferences import save_preference
enabled = self.enable_logging
save_preference("enable_logging", enabled)
configure_logging(enabled)
-152
View File
@@ -1,152 +0,0 @@
# thank you https://stackoverflow.com/a/71432759
from __future__ import annotations
from typing import Optional
from bpy.types import Image, Material
# Copyright (c) 2011, 2012, 2013, 2014, 2015, 2016 Jake Gordon and contributors
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
class Rectangle_Obj:
x: int = 0
y: int = 0
w: int = 0
h: int = 0
down: Rectangle_Obj = None
used: bool = False
right: Rectangle_Obj = None
def __init__(self, x:int, y:int, w:int, h:int, down=None, used =False, right=None):
self.x = x
self.y = y
self.w = w
self.h = h
self.down = down
self.used = used
self.right = right
def split(self, w, h) -> Rectangle_Obj:
self.used = True
self.down = Rectangle_Obj(x=self.x, y=self.y + h, w=self.w, h=self.h - h)
self.right = Rectangle_Obj(x=self.x + w, y=self.y, w=self.w - w, h=h)
return self
def find(self, w, h) -> Optional[Rectangle_Obj]:
if self.used:
return self.right.find(w, h) or self.down.find(w, h)
elif (w <= self.w) and (h <= self.h):
return self
return None
class MaterialImageList:
albedo: Image
normal: Image
emission: Image
ambient_occlusion: Image
height: Image
roughness: Image
fit: Rectangle_Obj
material: Material
def __init__(self):
pass
x: int = 0
y: int = 0
w: int = 0
h: int = 0
class BinPacker(object):
root: Rectangle_Obj
bin: list[MaterialImageList] = []
def __init__(self, structure: list[MaterialImageList]):
self.root = None
self.bin = structure
def fit(self):
structure = self.bin
structure_len = len(self.bin)
w: int = 0
h: int = 0
if structure_len > 0:
w = structure[0].w
h = structure[0].h
self.root = Rectangle_Obj(x=0, y=0, w=w, h=h)
for img in structure:
w = img.w
h = img.h
node = self.root.find(w, h)
if node:
img.fit = node.split(w, h)
else:
img.fit = self.grow_node(w, h)
return structure
def grow_node(self, w, h) -> Optional[Rectangle_Obj]:
can_grow_right = (h <= self.root.h)
can_grow_down = (w <= self.root.w)
should_grow_right = can_grow_right and (self.root.h >= (self.root.w + w))
should_grow_down = can_grow_down and (self.root.w >= (self.root.h + h))
if should_grow_right:
return self.grow_right(w, h)
elif should_grow_down:
return self.grow_down(w, h)
elif can_grow_right:
return self.grow_right(w, h)
elif can_grow_down:
return self.grow_down(w, h)
return None
def grow_right(self, w, h) -> Optional[Rectangle_Obj]:
self.root = Rectangle_Obj(
used=True,
x=0,
y=0,
w=self.root.w + w,
h=self.root.h,
down=self.root,
right=Rectangle_Obj(x=self.root.w, y=0, w=w, h=self.root.h))
node = self.root.find(w, h)
if node:
return node.split(w, h)
return None
def grow_down(self, w, h) -> Optional[Rectangle_Obj]:
self.root = Rectangle_Obj(
used=True,
x=0,
y=0,
w=self.root.w,
h=self.root.h + h,
down=Rectangle_Obj(x=0, y=self.root.h, w=self.root.w, h=h),
right=self.root
)
node = self.root.find(w, h)
if node:
return node.split(w, h)
return None
-4
View File
@@ -1,4 +0,0 @@
{
"language": 0,
"last_update_check": 1734295375.2681296
}
+370 -150
View File
@@ -1,169 +1,389 @@
import bpy
from ..functions.translations import t, get_languages_list, update_language
from ..core.register import register_property
from bpy.types import Scene, Object, Material, Context
from bpy.props import BoolProperty, EnumProperty, IntProperty, CollectionProperty, StringProperty, FloatVectorProperty, PointerProperty
from ..core.addon_preferences import get_preference
from ..core.common import SceneMatClass, MaterialListBool, get_armatures, get_mesh_items, get_armatures_that_are_not_selected
from typing import List, Tuple, Optional
from bpy.types import PropertyGroup, Material, Scene, Object, Context
from bpy.props import (
StringProperty,
BoolProperty,
EnumProperty,
IntProperty,
FloatProperty,
CollectionProperty,
PointerProperty
)
from .logging_setup import logger
from .translations import t, get_languages_list, update_language
from .addon_preferences import get_preference, save_preference
from .updater import get_version_list
from .common import get_armature_list, get_active_armature, get_all_meshes
from ..functions.visemes import VisemePreview
from ..functions.eye_tracking import set_rotation
def register() -> None:
default_language = get_preference("language", 0)
register_property((bpy.types.Scene, "avatar_toolkit_language", bpy.props.EnumProperty(
name=t("Settings.language.label", "Language"),
description=t("Settings.language.desc", "Select the language for the addon"),
items=get_languages_list,
default=default_language,
update=update_language
)))
def update_validation_mode(self, context):
logger.info(f"Updating validation mode to: {self.validation_mode}")
save_preference("validation_mode", self.validation_mode)
register_property((bpy.types.Scene, "selected_mesh", bpy.props.EnumProperty(
items=get_mesh_items,
name=t("VisemePanel.selected_mesh.label"),
description=t("VisemePanel.selected_mesh.desc")
)))
def update_logging_state(self, context):
logger.info(f"Updating logging state to: {self.enable_logging}")
save_preference("enable_logging", self.enable_logging)
from .logging_setup import configure_logging
configure_logging(self.enable_logging)
register_property((bpy.types.Object, "material_group_expanded", bpy.props.BoolProperty(
name="Expand Material Group",
description="Show/hide materials for this mesh",
default=False
)))
def update_shape_intensity(self, context):
if self.viseme_preview_mode:
from ..functions.visemes import VisemePreview
VisemePreview.update_preview(context)
register_property((bpy.types.Material, "material_expanded", bpy.props.BoolProperty(
name="Expand Material",
description="Show/hide material properties",
default=False
)))
register_property((bpy.types.Scene, "material_search_filter", bpy.props.StringProperty(
name="Search Materials",
description="Filter materials by name",
default=""
)))
register_property((bpy.types.Material, "include_in_atlas", bpy.props.BoolProperty(
name=t("TextureAtlas.include_in_atlas"),
description=t("TextureAtlas.include_in_atlas_desc"),
default=True
)))
register_property((bpy.types.Scene, "merge_armature_apply_transforms", bpy.props.BoolProperty(
default=False,
name=t("MergeArmature.merge_armatures.apply_transforms.label"),
description=t("MergeArmature.merge_armatures.apply_transforms.desc")
)))
register_property((bpy.types.Scene, "merge_armature_align_bones", bpy.props.BoolProperty(
default=False,
name=t("MergeArmature.merge_armatures.align_bones.label"),
description=t("MergeArmature.merge_armatures.align_bones.desc")
)))
class AvatarToolkitSceneProperties(PropertyGroup):
"""Property group containing Avatar Toolkit scene-level settings and properties"""
register_property((bpy.types.Scene, "avatar_toolkit_language_changed", bpy.props.BoolProperty(default=False)))
avatar_toolkit_updater_version_list: EnumProperty(
items=get_version_list,
name=t("Scene.avatar_toolkit_updater_version_list.name"),
description=t("Scene.avatar_toolkit_updater_version_list.description")
)
register_property((bpy.types.Scene, "avatar_toolkit_progress_steps", bpy.props.IntProperty(default=0)))
register_property((bpy.types.Scene, "avatar_toolkit_progress_current", bpy.props.IntProperty(default=0)))
active_armature: EnumProperty(
items=get_armature_list,
name=t("QuickAccess.select_armature"),
description=t("QuickAccess.select_armature"),
)
register_property((bpy.types.Scene, "avatar_toolkit_mouth_a", bpy.props.StringProperty(
name=t("VisemePanel.mouth_a.label"),
description=t("VisemePanel.mouth_a.desc")
)))
register_property((bpy.types.Scene, "avatar_toolkit_mouth_o", bpy.props.StringProperty(
name=t("VisemePanel.mouth_o.label"),
description=t("VisemePanel.mouth_o.desc")
)))
register_property((bpy.types.Scene, "avatar_toolkit_mouth_ch", bpy.props.StringProperty(
name=t("VisemePanel.mouth_ch.label"),
description=t("VisemePanel.mouth_ch.desc")
)))
register_property((bpy.types.Scene, "avatar_toolkit_shape_intensity", bpy.props.FloatProperty(
name=t("VisemePanel.shape_intensity"),
description=t("VisemePanel.shape_intensity_desc"),
language: EnumProperty(
name=t("Settings.language"),
description=t("Settings.language_desc"),
items=get_languages_list,
update=update_language
)
validation_mode: EnumProperty(
name=t("Settings.validation_mode"),
description=t("Settings.validation_mode_desc"),
items=[
('STRICT', t("Settings.validation_mode.strict"), t("Settings.validation_mode.strict_desc")),
('BASIC', t("Settings.validation_mode.basic"), t("Settings.validation_mode.basic_desc")),
('NONE', t("Settings.validation_mode.none"), t("Settings.validation_mode.none_desc"))
],
default=get_preference("validation_mode", "STRICT"),
update=update_validation_mode
)
enable_logging: BoolProperty(
name=t("Settings.enable_logging"),
description=t("Settings.enable_logging_desc"),
default=False,
update=update_logging_state
)
debug_expand: BoolProperty(
name="Debug Settings Expanded",
default=False
)
remove_doubles_merge_distance: FloatProperty(
name=t("Optimization.merge_distance"),
description=t("Optimization.merge_distance_desc"),
default=0.0001,
min=0.00001,
max=0.1
)
remove_doubles_advanced: BoolProperty(
name=t("Optimization.remove_doubles_advanced"),
description=t("Optimization.remove_doubles_advanced_desc"),
default=False
)
connect_bones_min_distance: FloatProperty(
name=t("Tools.connect_bones_min_distance"),
description=t("Tools.connect_bones_min_distance_desc"),
default=0.001,
min=0.0001,
max=0.1,
precision=4
)
merge_twist_bones: BoolProperty(
name=t("MMD.merge_twist_bones"),
description=t("MMD.merge_twist_bones_desc"),
default=True
)
keep_twist_bones: BoolProperty(
name=t("MMD.keep_twist_bones"),
description=t("MMD.keep_twist_bones_desc"),
default=False
)
keep_upper_chest: BoolProperty(
name=t("MMD.keep_upper_chest"),
description=t("MMD.keep_upper_chest_desc"),
default=True
)
merge_weights_threshold: FloatProperty(
name=t("MMD.merge_weights_threshold"),
description=t("MMD.merge_weights_threshold_desc"),
default=0.01,
min=0.0,
max=1.0
)
viseme_preview_mode: BoolProperty(
name=t("Visemes.preview_mode"),
description=t("Visemes.preview_mode_desc"),
default=False
)
viseme_preview_selection: StringProperty(
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
default="vrc.v_aa"
)
mouth_a: StringProperty(
name=t("Visemes.mouth_a"),
description=t("Visemes.mouth_a_desc")
)
mouth_o: StringProperty(
name=t("Visemes.mouth_o"),
description=t("Visemes.mouth_o_desc")
)
mouth_ch: StringProperty(
name=t("Visemes.mouth_ch"),
description=t("Visemes.mouth_ch_desc")
)
shape_intensity: FloatProperty(
name=t("Visemes.shape_intensity"),
description=t("Visemes.shape_intensity_desc"),
default=1.0,
min=0.0,
max=2.0
)))
register_property((bpy.types.Scene, "merge_twist_bones", bpy.props.BoolProperty(
name=t("Tools.merge_twist_bones.label"),
description=t("Tools.merge_twist_bones.desc"),
max=2.0,
precision=3,
update=update_shape_intensity
)
viseme_preview_selection: EnumProperty(
name=t("Visemes.preview_selection"),
description=t("Visemes.preview_selection_desc"),
items=[
('vrc.v_aa', 'AA', 'A as in "bat"'),
('vrc.v_ch', 'CH', 'Ch as in "choose"'),
('vrc.v_dd', 'DD', 'D as in "dog"'),
('vrc.v_ih', 'IH', 'I as in "bit"'),
('vrc.v_ff', 'FF', 'F as in "fox"'),
('vrc.v_e', 'E', 'E as in "bet"'),
('vrc.v_kk', 'KK', 'K as in "cat"'),
('vrc.v_nn', 'NN', 'N as in "net"'),
('vrc.v_oh', 'OH', 'O as in "hot"'),
('vrc.v_ou', 'OU', 'O as in "go"'),
('vrc.v_pp', 'PP', 'P as in "pat"'),
('vrc.v_rr', 'RR', 'R as in "red"'),
('vrc.v_sil', 'SIL', 'Silence'),
('vrc.v_ss', 'SS', 'S as in "sit"'),
('vrc.v_th', 'TH', 'Th as in "think"')
],
update=lambda s, c: VisemePreview.update_preview(c)
)
eye_tracking_type: EnumProperty(
name=t("EyeTracking.type"),
description=t("EyeTracking.type_desc"),
items=[
('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")),
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
],
default='AV3'
)
eye_mode: EnumProperty(
name=t("EyeTracking.mode"),
items=[
('CREATION', t("EyeTracking.mode.creation"), ""),
('TESTING', t("EyeTracking.mode.testing"), "")
],
default='CREATION'
)
eye_rotation_x: FloatProperty(
name=t("EyeTracking.rotation.x"),
update=set_rotation
)
eye_rotation_y: FloatProperty(
name=t("EyeTracking.rotation.y"),
update=set_rotation
)
mesh_name_eye: StringProperty(
name=t("EyeTracking.mesh_name"),
description=t("EyeTracking.mesh_name_desc")
)
head: StringProperty(
name=t("EyeTracking.head_bone"),
description=t("EyeTracking.head_bone_desc")
)
eye_left: StringProperty(
name=t("EyeTracking.eye_left"),
description=t("EyeTracking.eye_left_desc")
)
eye_right: StringProperty(
name=t("EyeTracking.eye_right"),
description=t("EyeTracking.eye_right_desc")
)
disable_eye_movement: BoolProperty(
name=t("EyeTracking.disable_movement"),
description=t("EyeTracking.disable_movement_desc"),
default=False
)
disable_eye_blinking: BoolProperty(
name=t("EyeTracking.disable_blinking"),
description=t("EyeTracking.disable_blinking_desc"),
default=False
)
eye_distance: FloatProperty(
name=t("EyeTracking.distance"),
description=t("EyeTracking.distance_desc"),
default=0.0,
min=-1.0,
max=1.0
)
iris_height: FloatProperty(
name=t("EyeTracking.iris_height"),
description=t("EyeTracking.iris_height_desc"),
default=0.0,
min=-1.0,
max=1.0
)
eye_blink_shape: FloatProperty(
name=t("EyeTracking.blink_shape"),
description=t("EyeTracking.blink_shape_desc"),
default=1.0,
min=0.0,
max=1.0
)
eye_lowerlid_shape: FloatProperty(
name=t("EyeTracking.lowerlid_shape"),
description=t("EyeTracking.lowerlid_shape_desc"),
default=1.0,
min=0.0,
max=1.0
)
wink_left: StringProperty(
name=t("EyeTracking.wink_left"),
description=t("EyeTracking.wink_left_desc")
)
wink_right: StringProperty(
name=t("EyeTracking.wink_right"),
description=t("EyeTracking.wink_right_desc")
)
lowerlid_left: StringProperty(
name=t("EyeTracking.lowerlid_left"),
description=t("EyeTracking.lowerlid_left_desc")
)
lowerlid_right: StringProperty(
name=t("EyeTracking.lowerlid_right"),
description=t("EyeTracking.lowerlid_right_desc")
)
merge_mode: EnumProperty(
name=t('CustomPanel.merge_mode'),
description=t('CustomPanel.merge_mode_desc'),
items=[
('ARMATURE', t('CustomPanel.mode.armature'), t('CustomPanel.mode.armature_desc')),
('MESH', t('CustomPanel.mode.mesh'), t('CustomPanel.mode.mesh_desc'))
],
default='ARMATURE'
)
merge_armature_into: StringProperty(
name=t('MergeArmature.into'),
description=t('MergeArmature.into_desc'),
default=""
)
merge_armature: StringProperty(
name=t('MergeArmature.from'),
description=t('MergeArmature.from_desc'),
default=""
)
attach_mesh: StringProperty(
name=t('AttachMesh.select'),
description=t('AttachMesh.select_desc'),
default=""
)
attach_bone: StringProperty(
name=t('AttachBone.select'),
description=t('AttachBone.select_desc'),
default=""
)
merge_all_bones: BoolProperty(
name=t('MergeArmature.merge_all'),
description=t('MergeArmature.merge_all_desc'),
default=True
)))
)
register_property((bpy.types.Scene, "selected_armature", bpy.props.EnumProperty(
items=get_armatures,
name=t("Quick_Access.selected_armature.label"),
description=t("Quick_Access.selected_armature.desc"),
default=0
)))
apply_transforms: BoolProperty(
name=t('MergeArmature.apply_transforms'),
description=t('MergeArmature.apply_transforms_desc'),
default=True
)
register_property((bpy.types.Scene, "merge_armature_source", bpy.props.EnumProperty(
items=get_armatures_that_are_not_selected,
name=t("MergeArmatures.selected_armature.label"),
description=t("MergeArmatures.selected_armature.label"),
default=0
)))
join_meshes: BoolProperty(
name=t('MergeArmature.join_meshes'),
description=t('MergeArmature.join_meshes_desc'),
default=True
)
register_property((bpy.types.Scene, "avatar_toolkit_updater_version_list", bpy.props.EnumProperty(
name=t('Scene.avatar_toolkit_updater_version_list.name'),
description=t('Scene.avatar_toolkit_updater_version_list.description'),
items=get_version_list
)))
#happy with how compressed this get_texture_node_list method is - @989onan
def get_texture_node_list(self: Material, context: Context) -> list[set[3]]:
if self.use_nodes:
Object.Enum = [((i.image.name if i.image else i.name+"_image"),(i.image.name if i.image else "node with no image..."),(i.image.name if i.image else i.name),index+1) for index,i in enumerate(self.node_tree.nodes) if i.bl_idname == "ShaderNodeTexImage"]
if not len(Object.Enum):
Object.Enum = [(t("TextureAtlas.error.label"), t("TextureAtlas.no_images_error.desc") , t("TextureAtlas.error.label"), 0)]
else:
Object.Enum = [(t("TextureAtlas.error.label"), t("TextureAtlas.no_nodes_error.desc"), t("TextureAtlas.error.label"), 0)]
Object.Enum.append((t("TextureAtlas.none.label"), t("TextureAtlas.none.label"), t("TextureAtlas.none.label"), 0))
return Object.Enum
register_property((Material, "texture_atlas_albedo", EnumProperty(
name=t("TextureAtlas.albedo"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()),
default=0,
items=get_texture_node_list)))
register_property((Material, "texture_atlas_normal", EnumProperty(
name=t("TextureAtlas.normal"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()),
default=0,
items=get_texture_node_list)))
register_property((Material, "texture_atlas_emission", EnumProperty(
name=t("TextureAtlas.emission"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()),
default=0,
items=get_texture_node_list)))
register_property((Material, "texture_atlas_ambient_occlusion", EnumProperty(
name=t("TextureAtlas.ambient_occlusion"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()),
default=0,
items=get_texture_node_list)))
register_property((Material, "texture_atlas_height", EnumProperty(
name=t("TextureAtlas.height"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()),
default=0,
items=get_texture_node_list)))
register_property((Material, "texture_atlas_roughness", EnumProperty(
name=t("TextureAtlas.roughness"),
description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()),
default=0,
items=get_texture_node_list)))
register_property((Scene, "texture_atlas_material_index", IntProperty(
default=-1,
get=(lambda self : -1),
set=(lambda self,context : None))))
remove_zero_weights: BoolProperty(
name=t('MergeArmature.remove_zero_weights'),
description=t('MergeArmature.remove_zero_weights_desc'),
default=True
)
register_property((Scene, "materials", CollectionProperty(type=SceneMatClass)))
cleanup_shape_keys: BoolProperty(
name=t('MergeArmature.cleanup_shape_keys'),
description=t('MergeArmature.cleanup_shape_keys_desc'),
default=True
)
register_property((Scene, "texture_atlas_Has_Mat_List_Shown", BoolProperty(
default=False,
get=MaterialListBool.get_bool,
set=MaterialListBool.set_bool)))
attach_mesh: StringProperty(
name=t("Tools.attach_mesh_select"),
description=t("Tools.attach_mesh_select_desc")
)
attach_bone: StringProperty(
name=t("Tools.attach_bone_select"),
description=t("Tools.attach_bone_select_desc")
)
def register() -> None:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
logger.debug("Properties registered successfully")
def unregister() -> None:
#if you register properties with register_property then you shouldn't need this function.
pass
"""Unregister the Avatar Toolkit property group"""
logger.info("Unregistering Avatar Toolkit properties")
del bpy.types.Scene.avatar_toolkit
logger.debug("Properties unregistered successfully")
-105
View File
@@ -1,105 +0,0 @@
import bpy
import typing
from typing import List, Type
# List to store the classes to register
__bl_classes = []
# List to store the ordered classes for registration
__bl_ordered_classes = []
# List to store props to register
__bl_props = []
def register_wrap(cls):
# Check if the class has a 'bl_rna' attribute (indicating it's a Blender class)
if hasattr(cls, 'bl_rna'):
# Add the class to the list of classes to register
__bl_classes.append(cls)
return cls
# Register all properties
def register_property(prop):
__bl_props.append(prop)
def register_properties():
for prop in __bl_props:
if isinstance(prop[2], bpy.props._PropertyDeferred):
setattr(prop[0], prop[1], prop[2])
else:
prop()
def clear_registration():
__bl_classes.clear()
__bl_ordered_classes.clear()
__bl_props.clear()
def unregister_properties():
for prop in reversed(__bl_props):
try:
delattr(prop[0], prop[1])
except AttributeError:
continue
clear_registration()
#- @989onan had to add this from Cats. This is extremely important else you will be screamed at by register order issues!
# Find order to register to solve dependencies
#################################################
def toposort(deps_dict):
sorted_list = []
sorted_values = set()
while len(deps_dict) > 0:
unsorted = []
for value, deps in deps_dict.items():
if len(deps) == 0:
sorted_list.append(value)
sorted_values.add(value)
else:
unsorted.append(value)
deps_dict = {value : deps_dict[value] - sorted_values for value in unsorted}
#sort_order(sorted_list) #to sort by 'bl_order' so we can choose how things may appear in the ui
return sorted_list
def order_classes():
deps_dict = {}
classes_to_register = set(iter_classes_to_register())
for class_obj in classes_to_register:
deps_dict[class_obj] = set(iter_own_register_deps(class_obj, classes_to_register))
__bl_ordered_classes.clear()
# Then put everything else sorted into the list
for class_obj in toposort(deps_dict):
__bl_ordered_classes.append(class_obj)
print(__bl_ordered_classes)
__bl_classes.clear()
def iter_classes_to_register():
for class_obj in __bl_classes:
yield class_obj
def iter_own_register_deps(class_obj, own_classes):
yield from (dep for dep in iter_register_deps(class_obj) if dep in own_classes)
def iter_register_deps(class_obj):
for value in typing.get_type_hints(class_obj, {}, {}, True).values():
dependency = get_dependency_from_annotation(value)
if dependency is not None:
yield dependency
if hasattr(class_obj, "bl_parent_id"):
if class_obj.bl_parent_id != "":
for dependency in __bl_classes:
if dependency.bl_idname == class_obj.bl_parent_id:
yield dependency
def get_dependency_from_annotation(value):
if isinstance(value, tuple) and len(value) == 2:
if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty):
return value[1]["type"]
return None
+112
View File
@@ -0,0 +1,112 @@
import os
import json
import bpy
import logging
from bpy.app.translations import locale
from typing import Dict, List, Tuple, Optional, Any
from ..core.logging_setup import logger
from .addon_preferences import save_preference, get_preference
# Set up logging
logger = logging.getLogger(__name__)
# Use __file__ to get the current file's directory
current_dir = os.path.dirname(os.path.abspath(__file__))
main_dir = os.path.dirname(current_dir)
resources_dir = os.path.join(main_dir, "resources")
translations_dir = os.path.join(resources_dir, "translations")
dictionary: Dict[str, str] = dict()
languages: List[str] = []
_translation_cache: Dict[str, Dict[str, str]] = {}
verbose: bool = True
def get_fallback_language() -> str:
"""Return the default fallback language"""
return "en_US"
def load_translations() -> bool:
"""Load translations for the selected language"""
global dictionary, languages
old_dictionary = dictionary.copy()
dictionary = dict()
languages = ["auto"]
# Populate languages list
for i in os.listdir(translations_dir):
lang = i.split(".")[0]
if lang != "auto":
languages.append(lang)
language_index: int = get_preference("language", 0)
logger.debug(f"Loading translations for language index: {language_index}")
if language_index == 0: # "auto"
language: str = bpy.context.preferences.view.language
else:
try:
language = languages[language_index]
except IndexError:
language = bpy.context.preferences.view.language
logger.debug(f"Selected language: {language}")
# Check cache first
if language in _translation_cache:
dictionary = _translation_cache[language]
return dictionary != old_dictionary
translation_file: str = os.path.join(translations_dir, language + ".json")
if os.path.exists(translation_file):
dictionary = _load_translation_file(translation_file)
else:
custom_language: str = language.split("_")[0]
custom_translation_file: str = os.path.join(translations_dir, custom_language + ".json")
if os.path.exists(custom_translation_file):
dictionary = _load_translation_file(custom_translation_file)
else:
logger.warning(f"Translation file not found for language: {language}")
default_file: str = os.path.join(translations_dir, get_fallback_language() + ".json")
if os.path.exists(default_file):
dictionary = _load_translation_file(default_file)
else:
logger.error("Default translation file not found")
_translation_cache[language] = dictionary
return dictionary != old_dictionary
def _load_translation_file(file_path: str) -> Dict[str, str]:
"""Load and parse a translation file"""
with open(file_path, 'r', encoding='utf-8') as file:
return json.load(file)["messages"]
def t(phrase: str, default: Optional[str] = None, **kwargs) -> str:
"""Get translation for a phrase with optional formatting"""
output: Optional[str] = dictionary.get(phrase)
if output is None:
if verbose:
logger.warning(f'Unknown phrase: {phrase}')
return default if default is not None else phrase
return output.format(**kwargs) if kwargs else output
def get_language_display_name(lang: str) -> str:
"""Get the display name for a language code"""
return t(f"Language.{lang}", lang)
def get_languages_list(self: Any, context: Any) -> List[Tuple[str, str, str]]:
"""Get list of available languages for UI"""
return [(str(i), get_language_display_name(lang), f"Use {lang} language")
for i, lang in enumerate(languages)]
def update_language(self: Any, context: Any) -> None:
"""Handle language update and UI refresh"""
logger.info(f"Updating language to: {self.language}")
save_preference("language", int(self.language))
load_translations()
context.scene.avatar_toolkit.language_changed = True
bpy.ops.avatar_toolkit.translation_restart_popup('INVOKE_DEFAULT')
# Initial load of translations
load_translations()
+44 -18
View File
@@ -10,10 +10,9 @@ import time
from urllib import request, error
from threading import Thread
from bpy.app.handlers import persistent
from ..functions.translations import t
from .translations import t
from .addon_preferences import get_preference, get_current_version, save_preference
from .register import register_wrap
from ..ui.panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..ui.main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from typing import Dict, List, Tuple, Optional, Set, Any
GITHUB_REPO = "teamneoneko/Avatar-Toolkit"
@@ -27,7 +26,7 @@ version_list: Optional[Dict[str, List[str]]] = None
main_dir: str = os.path.dirname(os.path.dirname(__file__))
downloads_dir: str = os.path.join(main_dir, "downloads")
@register_wrap
class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
bl_idname = 'avatar_toolkit.check_for_update'
bl_label = t('CheckForUpdateButton.label')
@@ -38,7 +37,7 @@ class AvatarToolkit_OT_CheckForUpdate(bpy.types.Operator):
check_for_update_background()
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_UpdateToLatest(bpy.types.Operator):
bl_idname = 'avatar_toolkit.update_latest'
bl_label = t('UpdateToLatestButton.label')
@@ -49,7 +48,7 @@ class AvatarToolkit_OT_UpdateToLatest(bpy.types.Operator):
update_now(latest=True)
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_UpdateNotificationPopup(bpy.types.Operator):
bl_idname = "avatar_toolkit.update_notification_popup"
bl_label = t('UpdateNotificationPopup.label')
@@ -69,7 +68,7 @@ class AvatarToolkit_OT_UpdateNotificationPopup(bpy.types.Operator):
col = layout.column(align=True)
col.label(text=t('UpdateNotificationPopup.newUpdate', default="New update available: {version}").format(version=latest_version_str))
@register_wrap
class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
bl_label = t("Updater.label")
bl_idname = "OBJECT_PT_avatar_toolkit_updater"
@@ -77,13 +76,14 @@ class AvatarToolkit_PT_UpdaterPanel(bpy.types.Panel):
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 9
bl_order = 8
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
draw_updater_panel(context, layout)
@register_wrap
class AvatarToolkit_OT_RestartBlenderPopup(bpy.types.Operator):
bl_idname = "avatar_toolkit.restart_blender_popup"
bl_label = t('RestartBlenderPopup.label', default="Restart Blender")
@@ -277,22 +277,48 @@ def get_version_list(self, context: bpy.types.Context) -> List[Tuple[str, str, s
return [(v, v, '') for v in version_list.keys()] if version_list else []
def draw_updater_panel(context: bpy.types.Context, layout: bpy.types.UILayout) -> None:
col = layout.column(align=True)
box = layout.box()
col = box.column(align=True)
# Header
row = col.row()
row.scale_y = 1.2
row.label(text=t('Updater.label'), icon='DOWNARROW_HLT')
col.separator()
# Update check/status section
if is_checking_for_update:
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label'))
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
text=t('Updater.CheckForUpdateButton.label'),
icon='SORTTIME')
elif update_needed:
col.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToLatestButton.label', name=latest_version_str))
update_row = col.row(align=True)
update_row.scale_y = 1.5
update_row.alert = True
update_row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname,
text=t('Updater.UpdateToLatestButton.label', name=latest_version_str),
icon='IMPORT')
else:
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname, text=t('Updater.CheckForUpdateButton.label_alt'))
col.operator(AvatarToolkit_OT_CheckForUpdate.bl_idname,
text=t('Updater.CheckForUpdateButton.label_alt'),
icon='FILE_REFRESH')
# Version selection section
col.separator()
row = col.row(align=True)
row.prop(context.scene, 'avatar_toolkit_updater_version_list', text='')
row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname, text=t('Updater.UpdateToSelectedButton.label'))
box_inner = col.box()
box_inner.label(text=t('Updater.selectVersion'), icon='SETTINGS')
row = box_inner.row(align=True)
row.prop(context.scene.avatar_toolkit, 'avatar_toolkit_updater_version_list', text='')
row.operator(AvatarToolkit_OT_UpdateToLatest.bl_idname,
text=t('Updater.UpdateToSelectedButton.label'),
icon='IMPORT')
# Current version info
col.separator()
col.label(text=t('Updater.currentVersion').format(name=get_current_version()))
curr_ver_row = col.row()
curr_ver_row.label(text=t('Updater.currentVersion').format(name=get_current_version()),
icon='CHECKMARK')
def ui_refresh() -> None:
for windowManager in bpy.data.window_managers: