Merge pull request #81 from Yusarina/Avatar-Toolkit-Overhaul

Avatar Toolkit Overhaul - Do not merge yet
This commit is contained in:
Yusarina
2024-12-16 13:00:15 +00:00
committed by GitHub
74 changed files with 7072 additions and 5173 deletions
+9 -51
View File
@@ -1,55 +1,13 @@
if "bpy" not in locals():
import bpy
from . import ui
from . import core
from . import functions
from .core import register
from .core.register import __bl_ordered_classes
from .core import properties
from .core import addon_preferences
from .core.updater import check_for_update_on_start
else:
import importlib
importlib.reload(ui)
importlib.reload(core)
importlib.reload(functions)
importlib.reload(properties)
importlib.reload(addon_preferences)
modules = None
ordered_classes = None
def register():
print("Registering Avatar Toolkit")
# Register the addon properties
properties.register()
# Load the translations
functions.translations.load_translations()
# Order the classes before registration
core.register.order_classes()
# Register the UI classes
for cls in core.register.__bl_ordered_classes:
print("registering " + str(cls))
bpy.utils.register_class(cls)
#finally register properties that may use some classes.
core.register.register_properties()
bpy.app.handlers.load_post.append(check_for_update_on_start)
from .functions.mesh_tools import AvatarToolkit_OT_ApplyShapeKey
bpy.types.MESH_MT_shape_key_context_menu.append((lambda self, context: self.layout.separator()))
bpy.types.MESH_MT_shape_key_context_menu.append((lambda self, context: self.layout.operator(AvatarToolkit_OT_ApplyShapeKey.bl_idname, icon="KEY_HLT")))
from .core import auto_load
print("Starting registration")
auto_load.init()
auto_load.register()
print("Registration complete")
def unregister():
print("Unregistering Avatar Toolkit")
# Unregister the UI classes
if check_for_update_on_start in bpy.app.handlers.load_post:
bpy.app.handlers.load_post.remove(check_for_update_on_start)
# Iterate over the classes to unregister in reverse order and unregister them
for cls in reversed(list(__bl_ordered_classes)):
bpy.utils.unregister_class(cls)
print("unregistering " + str(cls))
core.register.unregister_properties()
properties.unregister()
from .core import auto_load
auto_load.unregister()
-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
+531 -412
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
@@ -1,14 +1,10 @@
import bpy
from typing import List, Optional
from .common import get_armature
from ...core.common import get_active_armature
from bpy.types import Object, ShapeKey, Mesh, Context, Operator
from functools import lru_cache
from ..core.register import register_wrap
from ..functions.translations import t
from ...core.translations import t
@register_wrap
class AvatarToolKit_OT_ExportResonite(Operator):
bl_idname = 'avatar_toolkit.export_resonite'
bl_label = t("Importer.export_resonite.label")
@@ -16,10 +12,9 @@ class AvatarToolKit_OT_ExportResonite(Operator):
bl_options = {'REGISTER', 'UNDO'}
filepath: bpy.props.StringProperty()
@classmethod
def poll(cls, context: Context):
if get_armature(context) is None:
if get_active_armature(context) is None:
return False
return True
-54
View File
@@ -1,54 +0,0 @@
import bpy
# Importers which don't need much code should be added here, however if a importer needs alot of code
# Like the PMX and PMD importers, they should be added to their own files and referenced in the import_types str->lambda dictionary.
#See below comments on how the system works. - @989onan
import importlib.util
import os
import typing
from .import_pmx import import_pmx
from .import_pmd import import_pmd
if importlib.util.find_spec("io_scene_valvesource") is not None:
#from .....scripts.addons.io_scene_valvesource.import_smd import SmdImporter #<- use this to check if your IDE is working properly. idfk
from io_scene_valvesource.import_smd import SmdImporter #ignore IDE bitching this is fine, trust me, also above comment should be okay to an IDE usually if set up right. ^_^ - @989onan
def import_multi_files(method = None, directory: typing.Optional[str] = None, files: list[dict[str,str]] = None, filepath: typing.Optional[str] = ""):
if not files:
method(directory, filepath)
else:
for file in files:
fullpath = os.path.join(directory,os.path.basename(file["name"]))
print("run method!")
method(directory, fullpath)
#each import should map to a type. even in the case that multiple methods should import together, or have the same import method. Make sure the lambdas match so they get grouped together
#In the case of a file importer that takes only one file argument and each one needs individual import, use above method. (example of it in use is ".dae" format)
import_types: dict[str, typing.Callable[[str, list[dict[str,str]], str], None]] = {
"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):
names = ""
for importer in imports.keys():
names = names+"*."+importer+";"
return names
imports = concat_imports_filter(import_types)
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
-3
View File
@@ -1,3 +0,0 @@
{
"language": 0
}
+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:
-18
View File
@@ -1,18 +0,0 @@
from ..core.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)
-162
View File
@@ -1,162 +0,0 @@
import bpy
import math
from bpy.types import Context, Operator
from ..core.register import register_wrap
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes
from ..functions.translations import t
@register_wrap
class AvatarToolKit_OT_ApplyTransforms(Operator):
bl_idname = "avatar_toolkit.apply_transforms"
bl_label = t("Tools.apply_transforms.label")
bl_description = t("Tools.apply_transforms.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
meshes = get_all_meshes(context)
for mesh in meshes:
mesh.select_set(True)
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
self.report({'INFO'}, t("Tools.apply_transforms.success"))
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_ConnectBones(Operator):
bl_idname = "avatar_toolkit.connect_bones"
bl_label = t("Tools.connect_bones.label")
bl_description = t("Tools.connect_bones.desc")
bl_options = {'REGISTER', 'UNDO'}
min_distance: bpy.props.FloatProperty(
name=t("Tools.connect_bones.min_distance.label"),
description=t("Tools.connect_bones.min_distance.desc"),
default=0.005,
min=0.001,
max=0.1
)
@classmethod
def poll(cls, context: Context) -> bool:
return get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.connect_bones.invalid_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
bones_connected = 0
for bone in edit_bones:
if len(bone.children) == 1 and bone.name not in ['LeftEye', 'RightEye', 'Head', 'Hips']:
child = bone.children[0]
distance = math.dist(bone.head, child.head)
if distance > self.min_distance:
bone.tail = child.head
if bone.parent and len(bone.parent.children) == 1:
bone.use_connect = True
bones_connected += 1
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.connect_bones.success").format(bones_connected=bones_connected))
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, "min_distance")
@register_wrap
class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
bl_idname = "avatar_toolkit.delete_bone_constraints"
bl_label = t("Tools.delete_bone_constraints.label")
bl_description = t("Tools.delete_bone_constraints.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.delete_bone_constraints.invalid_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='POSE')
constraints_removed = 0
for bone in armature.pose.bones:
while bone.constraints:
bone.constraints.remove(bone.constraints[0])
constraints_removed += 1
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.delete_bone_constraints.success").format(constraints_removed=constraints_removed))
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_SeparateByMaterials(Operator):
bl_idname = "avatar_toolkit.separate_by_materials"
bl_label = t("Tools.separate_by_materials.label")
bl_description = t("Tools.separate_by_materials.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == 'MESH'
def execute(self, context: Context) -> set[str]:
obj = context.active_object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.separate(type='MATERIAL')
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_by_materials.success"))
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
bl_idname = "avatar_toolkit.separate_by_loose_parts"
bl_label = t("Tools.separate_by_loose_parts.label")
bl_description = t("Tools.separate_by_loose_parts.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.active_object and context.active_object.type == 'MESH'
def execute(self, context: Context) -> set[str]:
obj = context.active_object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.separate(type='LOOSE')
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_by_loose_parts.success"))
return {'FINISHED'}
-445
View File
@@ -1,445 +0,0 @@
import bpy
from ..core.register import register_wrap
from bpy.types import Context, Mesh, Panel, Operator, Armature, EditBone
from ..functions.translations import t
from ..core.common import get_selected_armature, get_all_meshes
from ..core import common
from ..core.dictionaries import bone_names
from mathutils import Matrix
@register_wrap
class AvatarToolkit_OT_StartPoseMode(Operator):
bl_idname = 'avatar_toolkit.start_pose_mode'
bl_label = t("Quick_Access.start_pose_mode.label")
bl_description = t("Quick_Access.start_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return get_selected_armature(context) != None and context.mode != "POSE"
def execute(self, context: Context) -> set[str]:
#give an active object so the next line doesn't throw an error.
context.view_layer.objects.active = get_selected_armature(context)
bpy.ops.object.mode_set(mode='OBJECT')
#deselect everything and select just our armature, then go into pose on just our selected armature. - @989onan
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = get_selected_armature(context)
context.view_layer.objects.active.select_set(True)
bpy.ops.object.mode_set(mode='POSE')
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_StopPoseMode(Operator):
bl_idname = 'avatar_toolkit.stop_pose_mode'
bl_label = t("Quick_Access.stop_pose_mode.label")
bl_description = t("Quick_Access.stop_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return get_selected_armature(context) != None and context.mode == "POSE"
def execute(self, context: Context) -> set[str]:
#this is done so that transforms are cleared but user selection is respected. - @989onan
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator):
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
bl_label = t("Quick_Access.apply_pose_as_shapekey.label")
bl_description = t("Quick_Access.apply_pose_as_shapekey.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = common.get_selected_armature(context)
return armature and context.mode == 'POSE'
def execute(self, context):
armature_obj = common.get_selected_armature(context)
mesh_objects = common.get_all_meshes(context)
for mesh_obj in mesh_objects:
if not mesh_obj.data:
continue
# Ensure basis exists
if not mesh_obj.data.shape_keys:
mesh_obj.shape_key_add(name='Basis')
# Store current pose as new shapekey
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False)
# Evaluate mesh in current pose
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh_obj.evaluated_get(depsgraph)
# Apply evaluated vertices to new shapekey
for i, v in enumerate(eval_mesh.data.vertices):
new_shape.data[i].co = v.co.copy()
# Reset pose
bpy.ops.pose.select_all(action='SELECT')
bpy.ops.pose.transforms_clear()
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t('Tools.apply_pose_as_rest.success'))
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
bl_label = t("Quick_Access.apply_pose_as_rest.label")
bl_description = t("Quick_Access.apply_pose_as_rest.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return get_selected_armature(context) != None and context.mode == "POSE"
def execute(self, context: Context):
if not common.apply_pose_as_rest(armature_obj=get_selected_armature(context),
meshes=get_all_meshes(context),
context=context):
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
return {'CANCELLED'}
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_RemoveZeroWeightBones(Operator):
bl_idname = "avatar_toolkit.remove_zero_weight_bones"
bl_label = t("Tools.remove_zero_weight_bones.label")
bl_description = t("Tools.remove_zero_weight_bones.desc")
bl_options = {'REGISTER', 'UNDO'}
threshold: bpy.props.FloatProperty(
default=0.01,
name=t("Tools.remove_zero_weight_bones.threshold.label"),
description=t("Tools.remove_zero_weight_bones.threshold.desc"),
min=0.0000001,
max=0.9999999)
@classmethod
def poll(cls, context: Context) -> bool:
return common.get_selected_armature(context) is not None
def execute(self, context: Context) -> set[str]:
armature = common.get_selected_armature(context)
if not common.is_valid_armature(armature):
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
return {'CANCELLED'}
weighted_bones: list[str] = []
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
# Store initial transforms
initial_transforms = {}
bpy.ops.object.mode_set(mode='EDIT')
for bone in armature.data.edit_bones:
initial_transforms[bone.name] = {
'head': bone.head.copy(),
'tail': bone.tail.copy(),
'roll': bone.roll,
'matrix': bone.matrix.copy(),
'parent': bone.parent.name if bone.parent else None
}
# Get weighted bones
armature.select_set(True)
context.view_layer.objects.active = armature
meshes = common.get_all_meshes(context)
for mesh in meshes:
mesh_data: Mesh = mesh.data
for vertex in mesh_data.vertices:
for group in vertex.groups:
if group.weight > self.threshold:
weighted_bones.append(mesh.vertex_groups[group.group].name)
bpy.ops.object.mode_set(mode='EDIT')
amature_data: Armature = armature.data
unweighted_bones: list[str] = []
# Identify unweighted bones
for bone in amature_data.edit_bones:
if bone.name not in weighted_bones:
unweighted_bones.append(bone.name)
# Process bone removal while preserving positions
for bone_name in unweighted_bones:
bone = amature_data.edit_bones[bone_name]
# Store children data
children = bone.children
children_data = {}
for child in children:
children_data[child.name] = initial_transforms[child.name]
# Reparent children
for child in children:
child.use_connect = False
if bone.parent:
child.parent = bone.parent
# Remove bone
amature_data.edit_bones.remove(bone)
# Restore children positions
for child_name, data in children_data.items():
if child_name in amature_data.edit_bones:
child = amature_data.edit_bones[child_name]
child.head = data['head']
child.tail = data['tail']
child.roll = data['roll']
child.matrix = data['matrix']
# Final position verification
for bone_name, transform in initial_transforms.items():
if bone_name in amature_data.edit_bones:
bone = amature_data.edit_bones[bone_name]
bone.matrix = transform['matrix']
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.remove_zero_weight_bones.success"))
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_MergeBonesToActive(Operator):
bl_idname = "avatar_toolkit.merge_bones_to_active"
bl_label = t("Tools.merge_bones_to_active.label")
bl_description = t("Tools.merge_bones_to_active.desc")
bl_options = {'REGISTER', 'UNDO'}
delete_old: bpy.props.BoolProperty(name=t("Tools.merge_bones_to_active.delete_old.label"), description=t("Tools.merge_bones_to_active.delete_old.desc"), default=False)
@classmethod
def poll(cls, context: Context) -> bool:
if common.get_selected_armature(context) is not None:
if common.get_selected_armature(context) == context.view_layer.objects.active:
if context.mode == "POSE":
return len(context.selected_pose_bones) > 1
elif context.mode == "EDIT_ARMATURE":
return len(context.selected_bones) > 1
return False
def execute(cls, context: Context) -> set[str]:
prev_mode: str = "EDIT"
if context.mode == "POSE":
prev_mode = "POSE"
#get active bone and a list of all other selected bones
bpy.ops.object.mode_set(mode='EDIT')
target_bone: str = context.active_bone.name
armature_data: Armature = context.view_layer.objects.active.data
bones: list[str] = [i.name for i in context.selected_bones]
bones.remove(target_bone)
for obj in common.get_all_meshes(context):
for bone in bones:
bone_name: str = armature_data.edit_bones[bone].name
common.transfer_vertex_weights(context=context,obj=obj,source_group=bone_name,target_group=armature_data.edit_bones[target_bone].name)
bpy.ops.object.mode_set(mode='EDIT')
for bone in bones:
if cls.delete_old:
for bone_child in armature_data.edit_bones[bone].children:
bone_child.parent = armature_data.edit_bones[bone].parent
armature_data.edit_bones.remove(armature_data.edit_bones[bone])
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_MergeBonesToParents(Operator):
bl_idname = "avatar_toolkit.merge_bones_to_parents"
bl_label = t("Tools.merge_bones_to_parents.label")
bl_description = t("Tools.merge_bones_to_parents.desc")
bl_options = {'REGISTER', 'UNDO'}
delete_old: bpy.props.BoolProperty(
name=t("Tools.merge_bones_to_parents.delete_old.label"),
description=t("Tools.merge_bones_to_parents.delete_old.desc"),
default=False
)
@classmethod
def poll(cls, context: Context) -> bool:
armature = common.get_selected_armature(context)
if armature and armature == context.view_layer.objects.active:
if context.mode == "POSE":
return len(context.selected_pose_bones) > 0
elif context.mode == "EDIT_ARMATURE":
return len(context.selected_editable_bones) > 0
return False
def execute(self, context: Context) -> set[str]:
prev_mode = context.mode
# Map 'EDIT_ARMATURE' to 'EDIT' for bpy.ops.object.mode_set
if prev_mode == 'EDIT_ARMATURE':
prev_mode = 'EDIT'
# Switch to Edit Mode
bpy.ops.object.mode_set(mode='EDIT')
armature_data: Armature = context.view_layer.objects.active.data
# Get selected bones in Edit Mode
selected_bones = context.selected_editable_bones
selected_bone_names = [bone.name for bone in selected_bones]
if not selected_bone_names:
self.report({'ERROR'}, t("No bones selected"))
return {'CANCELLED'}
for obj in common.get_all_meshes(context):
for bone_name in selected_bone_names:
bone = armature_data.edit_bones.get(bone_name)
if bone and bone.parent:
# Transfer weights from bone to its parent
common.transfer_vertex_weights(
context=context,
obj=obj,
source_group=bone_name,
target_group=bone.parent.name
)
# Ensure we're in Edit Mode after transfer
bpy.ops.object.mode_set(mode='EDIT')
else:
self.report({'WARNING'}, f"Bone '{bone_name}' has no parent or not found; skipping")
# Optionally delete old bones
if self.delete_old:
for bone_name in selected_bone_names:
bone = armature_data.edit_bones.get(bone_name)
if bone:
# Reassign children to the parent of the bone being deleted
for child in bone.children:
child.parent = bone.parent
# Remove the bone
armature_data.edit_bones.remove(bone)
else:
self.report({'WARNING'}, f"Bone '{bone_name}' not found in armature; cannot delete")
# Return to previous mode
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
@register_wrap
class AvatarToolkit_OT_MergeArmatures(Operator):
bl_idname = "avatar_toolkit.merge_armatures"
bl_label = t("MergeArmature.merge_armatures.label")
bl_description = t("MergeArmature.merge_armatures.desc").format(selected_armature_label=t("MergeArmatures.selected_armature.label"))
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return (common.get_selected_armature(context) is not None) and (common.get_merge_armature_source(context) is not None)
def make_active(self, obj: bpy.types.Object, context: Context):
context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = obj
obj.select_set(True)
def execute(cls, context: Context) -> set[str]:
source_armature: bpy.types.Object = bpy.data.objects[context.scene.merge_armature_source]
source_armature_data: Armature = source_armature.data
target_armature: bpy.types.Object = common.get_selected_armature(context)
target_armature_data: Armature = target_armature.data
parent_dictionary: dict[str, list[str]] = {}
cls.make_active(obj=source_armature, context=context)
if context.scene.merge_armature_apply_transforms:
target_armature.select_set(True)
for obj in target_armature.children:
obj.select_set(True)
for obj in source_armature.children:
obj.select_set(True)
bpy.ops.object.transform_apply()
if context.scene.merge_armature_align_bones:
if not context.scene.merge_armature_apply_transforms:
source_armature.matrix_world = target_armature.matrix_world
def children_bone_recursive(parent_bone) -> list[bpy.types.PoseBone]:
child_bones = []
child_bones.append(parent_bone)
for child in parent_bone.children:
child_bones.extend(children_bone_recursive(child))
return child_bones
bpy.ops.object.mode_set(mode='POSE')
source_armature_bone_names = [j.name for j in children_bone_recursive(
source_armature.pose.bones[
next(bone.name for bone in source_armature.pose.bones if common.simplify_bonename(bone.name) in bone_names['hips']) #Find bone that matches dictionary for hips before continuing.
]
)] #bones are default in order of parent child.
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = target_armature
bpy.ops.object.mode_set(mode='EDIT')
for source_bone_name in source_armature_bone_names:
if source_bone_name in target_armature_data.edit_bones:
obj = source_armature
editbone = target_armature_data.edit_bones[source_bone_name]
bone = obj.pose.bones[source_bone_name]
bone.matrix = editbone.matrix
else:
continue
if not common.apply_pose_as_rest(armature_obj=source_armature,meshes=[i for i in source_armature.children if i.type == 'MESH'], context=context):
cls.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
return {'FINISHED'}
cls.make_active(obj=source_armature, context=context)
bpy.ops.object.mode_set(mode='EDIT')
source_armature_data: Armature = source_armature.data
for bone_name in [i.name for i in source_armature_data.edit_bones]:
if bone_name in target_armature_data.bones:
parent_dictionary[bone_name] = [i.name for i in source_armature_data.edit_bones[bone_name].children]
source_armature_data.edit_bones.remove(source_armature_data.edit_bones[bone_name])
bpy.ops.object.mode_set(mode='OBJECT')
cls.make_active(obj=target_armature, context=context)
source_armature.select_set(True)
bpy.ops.object.join()
target_armature: bpy.types.Object = common.get_selected_armature(context)
cls.make_active(obj=target_armature, context=context)
bpy.ops.object.mode_set(mode='EDIT')
for bone_name, bone_name_list in parent_dictionary.items():
if bone_name in target_armature_data.edit_bones:
for bone_child in bone_name_list:
target_armature_data.edit_bones[bone_child].parent = target_armature_data.edit_bones[bone_name]
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
-298
View File
@@ -1,298 +0,0 @@
from pathlib import Path
import numpy
import bpy
import os
from typing import List, Tuple, Optional
from bpy.types import Material, Operator, Context, Object, Image, Mesh, MeshUVLoopLayer, Float2AttributeValue, ShaderNodeTexImage, ShaderNodeBsdfPrincipled, ShaderNodeNormalMap
from ..core.register import register_wrap
from ..core.common import SceneMatClass, MaterialListBool
from ..core.packer.rectangle_packer import MaterialImageList, BinPacker
from ..functions.translations import t
class MaterialImageList:
def __init__(self):
self.albedo: Image = None
self.normal: Image = None
self.emission: Image = None
self.ambient_occlusion: Image = None
self.height: Image = None
self.roughness: Image = None
self.material: Material = None
self.parent_mesh: Object = None
self.w: int = 0
self.h: int = 0
self.fit = None
def scale_images_to_largest(images: list[Image]) -> set:
x: int = 0
y: int = 0
# Filter out None or invalid images
valid_images = [img for img in images if img and img.has_data]
if not valid_images:
return 0, 0
for image in valid_images:
x = max(x, image.size[0])
y = max(y, image.size[1])
for image in valid_images:
image.scale(width=int(x), height=int(y))
return x, y
def MaterialImageList_to_Image_list(classitem: MaterialImageList) -> list[Image]:
list_of_images: list[Image] = []
list_of_images.append(classitem.albedo)
list_of_images.append(classitem.normal)
list_of_images.append(classitem.emission)
list_of_images.append(classitem.ambient_occlusion)
list_of_images.append(classitem.height)
list_of_images.append(classitem.roughness)
return list_of_images
def get_material_images_from_scene(context: Context) -> list[MaterialImageList]:
material_image_list: list[MaterialImageList] = []
for obj in context.scene.objects:
if obj.type == 'MESH':
for mat_slot in obj.material_slots:
# Only process materials that are selected for atlas
if mat_slot.material and mat_slot.material.include_in_atlas is True:
new_mat_image_item = MaterialImageList()
try:
new_mat_image_item.albedo = bpy.data.images[mat_slot.material.texture_atlas_albedo]
except Exception:
name = mat_slot.material.name + "_albedo_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.albedo = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.albedo.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
try:
new_mat_image_item.normal = bpy.data.images[mat_slot.material.texture_atlas_normal]
except Exception:
name = mat_slot.material.name + "_normal_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.normal = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.normal.pixels[:] = numpy.tile(numpy.array([0.5,0.5,1.0,1.0]), 32*32)
try:
new_mat_image_item.emission = bpy.data.images[mat_slot.material.texture_atlas_emission]
except Exception:
name = mat_slot.material.name + "_emission_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.emission = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.emission.pixels[:] = numpy.tile(numpy.array([0.0,0.0,0.0,1.0]), 32*32)
try:
new_mat_image_item.ambient_occlusion = bpy.data.images[mat_slot.material.texture_atlas_ambient_occlusion]
except Exception:
name = mat_slot.material.name + "_ambient_occlusion_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.ambient_occlusion = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.ambient_occlusion.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,1.0]), 32*32)
try:
new_mat_image_item.height = bpy.data.images[mat_slot.material.texture_atlas_height]
except Exception:
name = mat_slot.material.name + "_height_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.height = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.height.pixels[:] = numpy.tile(numpy.array([0.5,0.5,0.5,1.0]), 32*32)
try:
new_mat_image_item.roughness = bpy.data.images[mat_slot.material.texture_atlas_roughness]
except Exception:
name = mat_slot.material.name + "_roughness_replacement"
if name in bpy.data.images:
bpy.data.images.remove(image=bpy.data.images[name], do_unlink=True)
new_mat_image_item.roughness = bpy.data.images.new(name=name, width=32, height=32, alpha=True)
new_mat_image_item.roughness.pixels[:] = numpy.tile(numpy.array([1.0,1.0,1.0,0.0]), 32*32)
new_mat_image_item.material = mat_slot.material
new_mat_image_item.parent_mesh = obj
material_image_list.append(new_mat_image_item)
return material_image_list
def prep_images_in_scene(context: Context) -> list[MaterialImageList]:
preped_images: list[MaterialImageList] = get_material_images_from_scene(context)
for MaterialImageClass in preped_images:
ImageList: list[Image] = MaterialImageList_to_Image_list(MaterialImageClass)
MaterialImageClass.w, MaterialImageClass.h = scale_images_to_largest(ImageList)
return preped_images
@register_wrap
class AvatarToolKit_OT_AtlasMaterials(Operator):
bl_idname = "avatar_toolkit.atlas_materials"
bl_label = t("TextureAtlas.atlas_materials")
bl_description = t("TextureAtlas.atlas_materials_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.scene.texture_atlas_Has_Mat_List_Shown
def execute(self, context: Context) -> set:
try:
# Get only materials that are explicitly marked for inclusion
selected_materials = [m for m in prep_images_in_scene(context) if m.material and m.material.include_in_atlas is True]
if not selected_materials:
self.report({'WARNING'}, t("TextureAtlas.no_materials_selected"))
return {'CANCELLED'}
packer: BinPacker = BinPacker(selected_materials)
mat_images = packer.fit()
size: list[int] = [max([matimg.fit.w + matimg.albedo.size[0] for matimg in mat_images]),
max([matimg.fit.h + matimg.albedo.size[1] for matimg in mat_images])]
print([matimg.fit.w + matimg.albedo.size[1] for matimg in mat_images])
atlased_mat: MaterialImageList = MaterialImageList()
for mat in mat_images:
x: int = int(mat.fit.x)
y: int = int(mat.fit.y)
w: int = int(mat.albedo.size[0])
h: int = int(mat.albedo.size[1])
for obj in bpy.data.objects:
if obj.type == 'MESH':
mesh: Mesh = obj.data
for layer in mesh.polygons:
if obj.material_slots[layer.material_index].material:
if obj.material_slots[layer.material_index].material == mat.material:
for loop_idx in layer.loop_indices:
layer_loops: MeshUVLoopLayer
for layer_loops in mesh.uv_layers:
uv_item: Float2AttributeValue = layer_loops.uv[loop_idx]
uv_item.vector.x = (uv_item.vector.x*(w/size[0]))+(x/size[0])
uv_item.vector.y = (uv_item.vector.y*(h/size[1]))+(y/size[1])
for type in ["albedo","normal", "emission","ambient_occlusion","height", "roughness"]:
new_image_name: str= "Atlas_"+type+"_"+context.scene.name+"_"+Path(bpy.data.filepath).stem
print("Processing "+type+" atlas image")
if new_image_name in bpy.data.images:
bpy.data.images.remove(bpy.data.images[new_image_name])
canvas: Image = bpy.data.images.new(name=new_image_name, width=int(size[0]),height=int(size[1]), alpha=True)
c_w = canvas.size[0]
canvas_pixels: list[float] = list(canvas.pixels[:])
for mat in mat_images:
x: int = int(mat.fit.x)
y: int = int(mat.fit.y)
w: int = int(mat.albedo.size[0])
h: int = int(mat.albedo.size[1])
image_var: Image = eval("mat."+type)
image_pixels: list[float] = list(image_var.pixels[:])
print("writing image \""+image_var.name+"\" to canvas.")
print("x: \""+str(x)+"\" "+"y: \""+str(y)+"\" "+"w: \""+str(w)+"\" "+"h: \""+str(h)+"\" ")
for k in range(0,h):
for i in range(0, w):
for channel in range(0,4):
canvas_pixels[
int((((k+y)*c_w)
+
(i+x))*4)
+int(channel)
] = image_pixels[
int((
(k*w)
+i)*4)
+int(channel)]
canvas.pixels[:] = canvas_pixels[:]
canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath),new_image_name+".png"))
exec("atlased_mat."+type+" = canvas")
#I am sorry for the amount of nodes I'm instanciating here and their values.
#This is so that the nodes look pretty in the UI, which I think looks kinda nice. - @989onan
atlased_mat.material = bpy.data.materials.new(name="Atlas_Final_"+bpy.context.scene.name+"_"+Path(bpy.data.filepath).stem)
atlased_mat.material.use_nodes = True
atlased_mat.material.node_tree.nodes.clear()
principled_node: ShaderNodeBsdfPrincipled = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeBsdfPrincipled")
principled_node.location.x = 7.29706335067749
principled_node.location.y = 298.918212890625
output_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeOutputMaterial")
output_node.location.x = 297.29705810546875
output_node.location.y = 298.918212890625
albedo_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
albedo_node.location.x = -588.6177978515625
albedo_node.location.y = 414.1948547363281
albedo_node.image = atlased_mat.albedo
emission_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
emission_node.location.x = -588.6177978515625
emission_node.location.y = -173.9259033203125
emission_node.image = atlased_mat.emission
normal_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
normal_node.location.x = -941.4189453125
normal_node.location.y = -20.8391780853271
normal_node.image = atlased_mat.normal
normal_map_node: ShaderNodeNormalMap = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeNormalMap")
normal_map_node.location.x = -545.550537109375
normal_map_node.location.y = -0.7543716430664062
roughness_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
roughness_node.location.x = -592.1703491210938
roughness_node.location.y = 206.74075317382812
roughness_node.image = atlased_mat.roughness
ambient_occlusion_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
ambient_occlusion_node.location.x = -906.4371337890625
ambient_occlusion_node.location.y = -389.9602355957031
ambient_occlusion_node.image = atlased_mat.ambient_occlusion
height_node: ShaderNodeTexImage = atlased_mat.material.node_tree.nodes.new(type="ShaderNodeTexImage")
height_node.location.x = -1222.383056640625
height_node.location.y = -375.48406982421875
height_node.image = atlased_mat.height
atlased_mat.material.node_tree.links.new(principled_node.inputs["Base Color"], albedo_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Metallic"], roughness_node.outputs["Alpha"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Roughness"], roughness_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Alpha"], albedo_node.outputs["Alpha"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Normal"], normal_map_node.outputs["Normal"])
atlased_mat.material.node_tree.links.new(principled_node.inputs["Emission Color"], emission_node.outputs["Color"])
atlased_mat.material.node_tree.links.new(output_node.inputs["Surface"], principled_node.outputs["BSDF"])
atlased_mat.material.node_tree.links.new(normal_map_node.inputs["Color"], normal_node.outputs["Color"])
# Only update selected materials for meshes
for obj in context.scene.objects:
if obj.type == 'MESH':
mesh: Mesh = obj.data
for i, mat_slot in enumerate(obj.material_slots):
if mat_slot.material and mat_slot.material.include_in_atlas is True:
mesh.materials[i] = atlased_mat.material
self.report({'INFO'}, t("TextureAtlas.atlas_completed"))
return {"FINISHED"}
except Exception as e:
self.report({'ERROR'}, t("TextureAtlas.atlas_error"))
raise e
return {"FINISHED"}
-156
View File
@@ -1,156 +0,0 @@
import bpy
import re
from typing import List, Tuple, Optional, Set, Dict
from bpy.types import Material, Operator, Context, Object, NodeTree
from ..core.common import clean_material_names, get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
from ..core.register import register_wrap
from ..functions.translations import t
def textures_match(tex1: bpy.types.ImageTexture, tex2: bpy.types.ImageTexture) -> bool:
return tex1.image == tex2.image and tex1.extension == tex2.extension
def consolidate_nodes(node1: bpy.types.ShaderNodeTexImage, node2: bpy.types.ShaderNodeTexImage) -> None:
node2.color_space = node1.color_space
node2.coordinates = node1.coordinates
def copy_tex_nodes(mat1: Material, mat2: Material) -> None:
for node1 in mat1.node_tree.nodes:
if node1.type == 'TEX_IMAGE':
node2 = mat2.node_tree.nodes.get(node1.name)
if node2:
node2.mapping = node1.mapping
node2.projection = node1.projection
def consolidate_textures(node_tree1: NodeTree, node_tree2: NodeTree) -> None:
for node1 in node_tree1.nodes:
if node1.type == 'TEX_IMAGE':
for node2 in node_tree2.nodes:
if (node2.type == 'TEX_IMAGE' and
node1.image == node2.image):
consolidate_nodes(node1, node2)
node2.image = node1.image
elif node1.type == 'GROUP':
if node1.node_tree and node2.node_tree:
consolidate_textures(node1.node_tree, node2.node_tree)
def color_match(col1: Tuple[float, float, float, float], col2: Tuple[float, float, float, float], tolerance: float = 0.01) -> bool:
return all(abs(c1 - c2) < tolerance for c1, c2 in zip(col1, col2))
def materials_match(mat1: Material, mat2: Material, tolerance: float = 0.01) -> bool:
if not color_match(mat1.diffuse_color, mat2.diffuse_color, tolerance):
return False
if abs(mat1.roughness - mat2.roughness) > tolerance:
return False
if mat1.node_tree and mat2.node_tree:
consolidate_textures(mat1.node_tree, mat2.node_tree)
return True
def get_base_name(name: str) -> str:
mat_match = re.match(r"^(.*)\.\d{3}$", name)
return mat_match.group(1) if mat_match else name
@register_wrap
class AvatarToolKit_OT_CombineMaterials(Operator):
bl_idname = "avatar_toolkit.combine_materials"
bl_label = t("Optimization.combine_materials.label")
bl_description = t("Optimization.combine_materials.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> Set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'WARNING'}, t("Optimization.no_armature_selected"))
return {'CANCELLED'}
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='OBJECT')
meshes = get_all_meshes(context)
if not meshes:
self.report({'WARNING'}, t("Optimization.no_meshes_found"))
return {'CANCELLED'}
init_progress(context, 5) # 5 steps in total
update_progress(self, context, t("Optimization.consolidating_materials"))
num_combined = self.consolidate_materials(meshes)
update_progress(self, context, t("Optimization.cleaning_material_slots"))
cleaned_slots = self.clean_material_slots(meshes)
update_progress(self, context, t("Optimization.cleaning_material_names"))
cleaned_names = self.clean_material_names()
update_progress(self, context, t("Optimization.clearing_unused_data"))
removed_data_blocks = self.clear_unused_data_blocks()
update_progress(self, context, t("Optimization.finalizing"))
finish_progress(context)
self.report({'INFO'}, t("Optimization.materials_optimization_report").format(
num_combined=num_combined,
num_cleaned_slots=cleaned_slots,
num_cleaned_names=cleaned_names,
num_removed_data_blocks=removed_data_blocks
))
return {'FINISHED'}
def consolidate_materials(self, meshes: List[Object]) -> int:
mat_mapping: Dict[str, Material] = {}
num_combined: int = 0
for mesh in meshes:
for slot in mesh.material_slots:
mat: Optional[Material] = slot.material
if mat:
base_name: str = get_base_name(mat.name)
if base_name in mat_mapping:
base_mat: Material = mat_mapping[base_name]
try:
if materials_match(base_mat, mat):
consolidate_textures(base_mat.node_tree, mat.node_tree)
num_combined += 1
slot.material = base_mat
except AttributeError:
self.report({'WARNING'}, t("Optimization.material_attribute_mismatch").format(material_name=mat.name))
continue
else:
mat_mapping[base_name] = mat
return num_combined
def clean_material_slots(self, meshes: List[Object]) -> int:
cleaned_slots = 0
for obj in meshes:
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
initial_slots = len(obj.material_slots)
bpy.ops.object.material_slot_remove_unused()
cleaned_slots += initial_slots - len(obj.material_slots)
obj.select_set(False)
return cleaned_slots
def clean_material_names(self) -> int:
cleaned_names = 0
for obj in bpy.data.objects:
if obj.type == 'MESH':
result = clean_material_names(obj)
if result is not None:
cleaned_names += result
return cleaned_names
def clear_unused_data_blocks(self) -> int:
initial_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
bpy.ops.outliner.orphans_purge(do_local_ids=True, do_linked_ids=True, do_recursive=True)
final_count = sum(len(getattr(bpy.data, attr)) for attr in dir(bpy.data) if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection))
return initial_count - final_count
+459
View File
@@ -0,0 +1,459 @@
import bpy
import numpy as np
from typing import List, Optional, Dict, Set
from bpy.types import Context, Object, Operator
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_all_meshes,
fix_zero_length_bones,
clear_unused_data_blocks,
join_mesh_objects,
remove_unused_shapekeys
)
class AvatarToolkit_OT_MergeArmature(Operator):
bl_idname = 'avatar_toolkit.merge_armatures'
bl_label = t('MergeArmature.label')
bl_description = t('MergeArmature.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return len(get_all_meshes(context)) > 1
def execute(self, context):
try:
wm = context.window_manager
wm.progress_begin(0, 100)
# Get both armatures
base_armature_name = context.scene.merge_armature_into
merge_armature_name = context.scene.merge_armature
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
self.report({'ERROR'}, t('MergeArmature.error.not_found', name=merge_armature_name))
return {'CANCELLED'}
# Remove Rigid Bodies and Joints
delete_rigidbodies_and_joints(base_armature)
delete_rigidbodies_and_joints(merge_armature)
wm.progress_update(40)
# Check parents and transformations
if not validate_parents_and_transforms(merge_armature, base_armature, context):
wm.progress_end()
return {'CANCELLED'}
wm.progress_update(80)
# Get settings from scene properties
merge_all_bones = context.scene.avatar_toolkit.merge_all_bones
join_meshes = context.scene.avatar_toolkit.join_meshes
# Merge armatures
merge_armatures(
base_armature_name,
merge_armature_name,
mesh_only=False,
merge_all_bones=context.scene.avatar_toolkit.merge_all_bones,
join_meshes=join_meshes,
operator=self
)
wm.progress_update(90)
wm.progress_update(100)
wm.progress_end()
self.report({'INFO'}, t('MergeArmature.success'))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error merging armatures: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def delete_rigidbodies_and_joints(armature: Object):
"""Delete rigid bodies and joints associated with the armature."""
to_delete = []
parent = armature
while parent.parent:
parent = parent.parent
for child in parent.children:
if 'rigidbodies' in child.name.lower() or 'joints' in child.name.lower():
to_delete.append(child)
for grandchild in child.children:
if 'rigidbodies' in grandchild.name.lower() or 'joints' in grandchild.name.lower():
to_delete.append(grandchild)
for obj in to_delete:
bpy.data.objects.remove(obj, do_unlink=True)
def validate_parents_and_transforms(merge_armature: Object, base_armature: Object, context: Context) -> bool:
"""Validate parents and transformations of armatures before merging."""
merge_parent = merge_armature.parent
base_parent = base_armature.parent
if merge_parent or base_parent:
if context.scene.merge_all_bones:
for armature, parent in [(merge_armature, merge_parent), (base_armature, base_parent)]:
if parent:
if not is_transform_clean(parent):
logger.error("Parent transforms are not clean")
return False
bpy.data.objects.remove(parent, do_unlink=True)
else:
logger.error("Parent relationships need fixing")
return False
return True
def is_transform_clean(obj: Object) -> bool:
"""Check if an object's transforms are at default values."""
for i in range(3):
if obj.scale[i] != 1 or obj.location[i] != 0 or obj.rotation_euler[i] != 0:
return False
return True
def prepare_mesh_vertex_groups(mesh: Object):
"""Prepare mesh by assigning all vertices to a new vertex group."""
if mesh.vertex_groups:
for vg in mesh.vertex_groups:
mesh.vertex_groups.remove(vg)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh.name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
def merge_armatures(
base_armature_name: str,
merge_armature_name: str,
mesh_only: bool,
merge_all_bones: bool = False,
join_meshes: bool = False,
operator=None
):
"""Main function to merge two armatures."""
logger.info(f"Merging armatures: {merge_armature_name} into {base_armature_name}")
tolerance = 0.00008726647 # around 0.005 degrees
base_armature = bpy.data.objects.get(base_armature_name)
merge_armature = bpy.data.objects.get(merge_armature_name)
if not base_armature or not merge_armature:
logger.error(f"Armature not found: {merge_armature_name}")
if operator:
operator.report({'ERROR'}, t('MergeArmature.error.notFound', name=merge_armature_name))
return
# Check transforms early
if not validate_merge_armature_transforms(base_armature, merge_armature, None, tolerance):
if not bpy.context.scene.avatar_toolkit.apply_transforms:
logger.error("Transforms not aligned - user notification sent")
if operator:
operator.report({'ERROR'}, t('MergeArmature.error.transforms_not_aligned'))
return
# Apply transforms if enabled
if bpy.context.scene.avatar_toolkit.apply_transforms:
for obj in [base_armature, merge_armature]:
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
obj.select_set(False)
# Validate and fix armatures
fix_zero_length_bones(base_armature)
fix_zero_length_bones(merge_armature)
# Store original parent relationships
original_parents = {}
for bone in merge_armature.data.bones:
original_parents[bone.name] = bone.parent.name if bone.parent else None
# Get base bone names
base_bone_names = set(bone.name for bone in base_armature.data.bones)
# Switch to edit mode on merge armature and rename bones
bpy.context.view_layer.objects.active = merge_armature
bpy.ops.object.mode_set(mode='EDIT')
# Handle bone renaming based on merge_all_bones setting
for bone in merge_armature.data.edit_bones:
if not merge_all_bones:
# Only rename bones that don't exist in base armature
if bone.name not in base_bone_names:
bone.name += '.merge'
else:
# Rename all bones from merge armature
bone.name += '.merge'
# Return to object mode
bpy.ops.object.mode_set(mode='OBJECT')
# Select and join armatures
bpy.ops.object.select_all(action='DESELECT')
base_armature.select_set(True)
merge_armature.select_set(True)
bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.join()
# Restore parent relationships
bpy.ops.object.mode_set(mode='EDIT')
for bone in base_armature.data.edit_bones:
base_name = bone.name.replace('.merge', '')
if base_name in original_parents:
parent_name = original_parents[base_name]
if parent_name:
parent_bone = base_armature.data.edit_bones.get(parent_name)
if parent_bone:
bone.parent = parent_bone
bpy.ops.object.mode_set(mode='OBJECT')
# Update mesh parenting
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.parent == merge_armature:
obj.parent = base_armature
# Process vertex groups if not mesh_only
if not mesh_only:
meshes = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
process_vertex_groups(meshes)
# Remove zero weight vertex groups if enabled
if bpy.context.scene.avatar_toolkit.remove_zero_weights:
bpy.context.view_layer.objects.active = base_armature
for mesh in meshes:
bpy.context.view_layer.objects.active = mesh
bpy.ops.avatar_toolkit.clean_weights()
# Join meshes if requested
if join_meshes:
meshes_to_join = [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == base_armature]
if meshes_to_join:
joined_mesh = join_mesh_objects(bpy.context, meshes_to_join)
if joined_mesh:
logger.info(f"Joined meshes into {joined_mesh.name}")
# Clean up shape keys if enabled
if bpy.context.scene.avatar_toolkit.cleanup_shape_keys:
for obj in bpy.data.objects:
if obj.type == 'MESH' and obj.parent == base_armature:
remove_unused_shapekeys(obj)
# Remove any remaining .merge bones
bpy.context.view_layer.objects.active = base_armature
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = base_armature.data.edit_bones
bones_to_remove = [bone for bone in edit_bones if bone.name.endswith('.merge')]
for bone in bones_to_remove:
edit_bones.remove(bone)
bpy.ops.object.mode_set(mode='OBJECT')
# Final cleanup
clear_unused_data_blocks()
def validate_merge_armature_transforms(
base_armature: Object,
merge_armature: Object,
mesh_merge: Optional[Object],
tolerance: float
) -> bool:
"""Validate transforms of both armatures and mesh."""
for i in [0, 1, 2]:
if abs(base_armature.scale[i] - merge_armature.scale[i]) > tolerance:
return False
if abs(merge_armature.rotation_euler[i]) > tolerance or \
(mesh_merge and abs(mesh_merge.rotation_euler[i]) > tolerance):
return False
return True
def adjust_merge_armature_transforms(
merge_armature: Object,
mesh_merge: Object
):
"""Adjust transforms of the merge armature."""
old_loc = list(merge_armature.location)
old_scale = list(merge_armature.scale)
for i in [0, 1, 2]:
merge_armature.location[i] = (mesh_merge.location[i] * old_scale[i]) + old_loc[i]
merge_armature.rotation_euler[i] = mesh_merge.rotation_euler[i]
merge_armature.scale[i] = mesh_merge.scale[i] * old_scale[i]
for i in [0, 1, 2]:
mesh_merge.location[i] = 0
mesh_merge.rotation_euler[i] = 0
mesh_merge.scale[i] = 1
def detect_bones_to_merge(
base_edit_bones: bpy.types.ArmatureEditBones,
merge_edit_bones: bpy.types.ArmatureEditBones,
tolerance: float,
merge_all_bones: bool
) -> List[str]:
"""Detect corresponding bones between base and merge armatures using smart detection and position tolerance."""
bones_to_merge = []
# Cache base bone positions
base_bones_positions = {
bone.name: np.array(bone.head) for bone in base_edit_bones
}
# Smart bone detection
for merge_bone in merge_edit_bones:
merge_bone_position = np.array(merge_bone.head)
found_match = False
if merge_all_bones and merge_bone.name in base_bones_positions:
# If merging same bones by name
bones_to_merge.append(merge_bone.name)
found_match = True
else:
# Find bones with close positions
for base_bone_name, base_bone_position in base_bones_positions.items():
if np.linalg.norm(merge_bone_position - base_bone_position) <= tolerance:
bones_to_merge.append(base_bone_name)
found_match = True
break
if not found_match:
# Handle unmatched bones if needed
pass
return bones_to_merge
def process_vertex_groups(meshes: List[Object]):
"""Process vertex groups in meshes."""
for mesh in meshes:
vg_names = {vg.name for vg in mesh.vertex_groups}
merge_vg_names = [vg_name for vg_name in vg_names if vg_name.endswith('.merge')]
for vg_merge_name in merge_vg_names:
base_name = vg_merge_name[:-6]
vg_merge = mesh.vertex_groups.get(vg_merge_name)
vg_base = mesh.vertex_groups.get(base_name)
if vg_merge is None:
continue
if vg_base:
mix_vertex_groups(mesh, vg_merge_name, base_name)
else:
vg_merge.name = base_name
def mix_vertex_groups(mesh: Object, vg_from_name: str, vg_to_name: str):
"""Mix vertex group weights."""
vg_from = mesh.vertex_groups.get(vg_from_name)
vg_to = mesh.vertex_groups.get(vg_to_name)
if not vg_from or not vg_to:
return
num_vertices = len(mesh.data.vertices)
weights_from = np.zeros(num_vertices)
weights_to = np.zeros(num_vertices)
idx_from = vg_from.index
idx_to = vg_to.index
for v in mesh.data.vertices:
for g in v.groups:
if g.group == idx_from:
weights_from[v.index] = g.weight
elif g.group == idx_to:
weights_to[v.index] = g.weight
weights_combined = np.clip(weights_from + weights_to, 0.0, 1.0)
vg_to.add(range(num_vertices), weights_combined.tolist(), 'REPLACE')
mesh.vertex_groups.remove(vg_from)
def remove_unused_vertex_groups(mesh: Object):
"""Remove vertex groups with no weights."""
for vg in mesh.vertex_groups:
has_weights = False
for vert in mesh.data.vertices:
for group in vert.groups:
if group.group == vg.index and group.weight > 0.001:
has_weights = True
break
if has_weights:
break
if not has_weights:
mesh.vertex_groups.remove(vg)
def apply_armature_to_mesh(armature: Object, mesh: Object):
"""Apply armature deformation to mesh."""
armature_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature
if bpy.app.version >= (3, 5):
mesh.modifiers.move(mesh.modifiers.find(armature_mod.name), 0)
else:
for _ in range(len(mesh.modifiers) - 1):
bpy.ops.object.modifier_move_up(modifier=armature_mod.name)
with bpy.context.temp_override(object=mesh):
bpy.ops.object.modifier_apply(modifier=armature_mod.name)
def apply_armature_to_mesh_with_shapekeys(armature: Object, mesh: Object, context: Context):
"""Apply armature deformation to mesh with shape keys."""
old_active_index = mesh.active_shape_key_index
old_show_only = mesh.show_only_shape_key
mesh.show_only_shape_key = True
shape_keys = mesh.data.shape_keys.key_blocks
vertex_groups = []
mutes = []
for sk in shape_keys:
vertex_groups.append(sk.vertex_group)
sk.vertex_group = ''
mutes.append(sk.mute)
sk.mute = False
disabled_mods = []
for mod in mesh.modifiers:
if mod.show_viewport:
mod.show_viewport = False
disabled_mods.append(mod)
arm_mod = mesh.modifiers.new('PoseToRest', 'ARMATURE')
arm_mod.object = armature
co_length = len(mesh.data.vertices) * 3
eval_cos = np.empty(co_length, dtype=np.single)
for i, shape_key in enumerate(shape_keys):
mesh.active_shape_key_index = i
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh.evaluated_get(depsgraph)
eval_mesh.data.vertices.foreach_get('co', eval_cos)
shape_key.data.foreach_set('co', eval_cos)
if i == 0:
mesh.data.vertices.foreach_set('co', eval_cos)
for mod in disabled_mods:
mod.show_viewport = True
mesh.modifiers.remove(arm_mod)
for sk, vg, mute in zip(shape_keys, vertex_groups, mutes):
sk.vertex_group = vg
sk.mute = mute
mesh.active_shape_key_index = old_active_index
mesh.show_only_shape_key = old_show_only
+130
View File
@@ -0,0 +1,130 @@
import bpy
from bpy.types import Operator, Context, Object
from mathutils import Vector
from typing import Set, Optional
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_active_armature,
validate_armature,
get_all_meshes,
ProgressTracker,
calculate_bone_orientation,
add_armature_modifier
)
class AvatarToolkit_OT_AttachMesh(Operator):
"""Attach a mesh to an armature bone with automatic weight setup"""
bl_idname = "avatar_toolkit.attach_mesh"
bl_label = t("AttachMesh.label")
bl_description = t("AttachMesh.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
return armature is not None and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting mesh attachment process")
mesh_name = context.scene.avatar_toolkit.attach_mesh
armature = get_active_armature(context)
attach_bone_name = context.scene.avatar_toolkit.attach_bone
mesh = bpy.data.objects.get(mesh_name)
with ProgressTracker(context, 10, "Attaching Mesh") as progress:
# Validation steps
is_valid, error_msg = validate_mesh_transforms(mesh)
if not is_valid:
raise ValueError(error_msg)
progress.step(t("AttachMesh.validate_transforms"))
is_valid, error_msg = validate_mesh_name(armature, mesh_name)
if not is_valid:
raise ValueError(error_msg)
progress.step(t("AttachMesh.validate_name"))
# Parent mesh to armature
mesh.parent = armature
mesh.parent_type = 'OBJECT'
progress.step(t("AttachMesh.parent_mesh"))
# Setup vertex groups
if mesh.vertex_groups:
for vg in mesh.vertex_groups:
mesh.vertex_groups.remove(vg)
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg = mesh.vertex_groups.new(name=mesh_name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
progress.step(t("AttachMesh.setup_weights"))
# Create and setup bone
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
attach_to_bone = armature.data.edit_bones.get(attach_bone_name)
if not attach_to_bone:
raise ValueError(t("AttachMesh.error.bone_not_found", bone=attach_bone_name))
mesh_bone = armature.data.edit_bones.new(mesh_name)
mesh_bone.parent = attach_to_bone
progress.step(t("AttachMesh.create_bone"))
# Calculate bone placement
verts_in_group = [v for v in mesh.data.vertices
for g in v.groups if g.group == vg.index]
dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
# Set bone position and orientation
center = Vector((0, 0, 0))
for v in verts_in_group:
center += mesh.data.vertices[v.index].co
center /= len(verts_in_group)
mesh_bone.head = center
mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z)))
mesh_bone.roll = roll_angle
progress.step(t("AttachMesh.position_bone"))
bpy.ops.object.mode_set(mode='OBJECT')
add_armature_modifier(mesh, armature)
progress.step(t("AttachMesh.add_modifier"))
logger.info(f"Successfully attached mesh {mesh_name} to bone {attach_bone_name}")
self.report({'INFO'}, t("AttachMesh.success"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to attach mesh: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def validate_mesh_transforms(mesh):
"""Validate mesh transforms are suitable for attaching."""
if not mesh:
return False, "Mesh not found"
# Check for non-uniform scale
scale = mesh.scale
if abs(scale[0] - scale[1]) > 0.001 or abs(scale[1] - scale[2]) > 0.001:
return False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)"
return True, ""
def validate_mesh_name(armature, mesh_name):
"""Validate mesh name doesn't conflict with existing bones."""
if mesh_name in armature.data.bones:
return False, f"Bone named '{mesh_name}' already exists in armature"
return True, ""
-119
View File
@@ -1,119 +0,0 @@
import bpy
from ..core import common
from ..core import register_wrap
from .translations import t
import re
@register_wrap
class AvatarToolKit_OT_CreateDigitigradeLegs(bpy.types.Operator):
bl_idname = "avatar_toolkit.create_digitigrade_legs"
bl_label = t('Tools.create_digitigrade_legs.label')
bl_description = t('Tools.create_digitigrade_legs.desc')
@classmethod
def poll(cls, context):
if(context.active_object is None):
return False
if(context.selected_editable_bones is not None):
if(len(context.selected_editable_bones) == 2):
return True
return False
def execute(self, context):
for digi0 in context.selected_editable_bones:
digi1: bpy.types.EditBone = None
digi2: bpy.types.EditBone = None
digi3: bpy.types.EditBone = None
try:
digi1 = digi0.children[0]
digi2 = digi1.children[0]
digi3 = digi2.children[0]
except:
self.report({'ERROR'}, t('Tools.digitigrade_legs.error.bone_format'))
return {'CANCELLED'}
digi4 = None
try:
digi4 = digi3.children[0]
except:
print("no toe bone. Continuing.")
digi0.select = True
digi1.select = True
digi2.select = True
digi3.select = True
if(digi4):
digi4.select = True
bpy.ops.armature.roll_clear()
bpy.ops.armature.select_all(action='DESELECT')
#creating transform for upper leg
digi0.select = True
bpy.ops.transform.create_orientation(name="Toolkit_digi0", overwrite=True)
bpy.ops.armature.select_all(action='DESELECT')
#duplicate digi0 and assign it to thigh
thigh = common.duplicatebone(digi0)
bpy.ops.armature.select_all(action='DESELECT')
#make digi2 parrallel to digi1
digi2.align_orientation(digi0)
#extrude thigh
thigh.select_tail = True
bpy.ops.armature.extrude_move(ARMATURE_OT_extrude={"forked":False},TRANSFORM_OT_translate=None)
#set new bone to calf varible
bpy.ops.armature.select_more()
calf = context.selected_bones[0]
bpy.ops.armature.select_all(action='DESELECT')
#set calf end to digi2 end
calf.tail = digi2.tail
#make copy of calf, flip it, and then align bone so that it's head is moved to match in align phase
flipedcalf = common.duplicatebone(calf)
bpy.ops.armature.select_all(action='DESELECT')
flipedcalf.select = True
bpy.ops.armature.switch_direction()
bpy.ops.armature.select_all(action='DESELECT')
flippeddigi1 = common.duplicatebone(digi1)
bpy.ops.armature.select_all(action='DESELECT')
flippeddigi1.select = True
bpy.ops.armature.switch_direction()
bpy.ops.armature.select_all(action='DESELECT')
#align flipped calf to flipped middle leg to move the head
flipedcalf.align_orientation(flippeddigi1)
flipedcalf.length = flippeddigi1.length
#assign calf tail to flipped calf head so it moves calf's tail to be out at the perfect parallelagram
calf.head = flipedcalf.tail
#delete helper bones
bpy.ops.armature.select_all(action='DESELECT')
flippeddigi1.select = True
bpy.ops.armature.delete()
bpy.ops.armature.select_all(action='DESELECT')
flipedcalf.select = True
bpy.ops.armature.delete()
bpy.ops.armature.select_all(action='DESELECT')
#reparent the foot to the new calf so it will be part of the new foot IK chain
digi3.parent = calf
#Tada! It's done! now to rename the old 3 segments that make up the old part to noik so resonite doesn't try to select them
digi0.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi0.name)+"<noik>"
digi1.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi1.name)+"<noik>"
digi2.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",digi2.name)+"<noik>"
#finally fully done!
self.report({'INFO'}, t('Tools.digitigrade_legs.success'))
return {'FINISHED'}
+945
View File
@@ -0,0 +1,945 @@
import os
import bpy
import copy
import math
import bmesh
import mathutils
import json
from bpy.types import Operator, Object, Context
from typing import Optional, Dict, Tuple, Set
from collections import OrderedDict
from random import random
from itertools import chain
from ..core.logging_setup import logger
from ..core.translations import t
from ..core.common import (
ProgressTracker,
get_active_armature,
get_all_meshes,
get_armature_list,
validate_armature,
validate_mesh_for_pose,
cache_vertex_positions,
apply_vertex_positions
)
VALID_EYE_NAMES = {
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
'right': ['RightEye', 'Eye_R', 'eye_R', 'eye.R', 'EyeRight', 'right_eye', 'r_eye']
}
class CreateEyesAV3Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat Avatar 3.0"""
bl_idname = 'avatar_toolkit.create_eye_tracking_av3'
bl_label = t('EyeTracking.create.av3.label')
bl_description = t('EyeTracking.create.av3.desc')
bl_options = {'REGISTER', 'UNDO'}
mesh = None
@classmethod
def poll(cls, context):
toolkit = context.scene.avatar_toolkit
if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right:
return False
return True
def execute(self, context):
toolkit = context.scene.avatar_toolkit
armature = get_active_armature(context)
with ProgressTracker(context, 100, "Creating AV3 Eye Tracking") as progress:
try:
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
progress.step("Setting up bones")
# Set up bones
head = armature.data.edit_bones.get(toolkit.head)
old_eye_left = armature.data.edit_bones.get(toolkit.eye_left)
old_eye_right = armature.data.edit_bones.get(toolkit.eye_right)
# Store original names and transformations
left_name = old_eye_left.name
right_name = old_eye_right.name
left_matrix = old_eye_left.matrix.copy()
right_matrix = old_eye_right.matrix.copy()
left_length = old_eye_left.length
right_length = old_eye_right.length
# Unparent and remove original bones
old_eye_left.parent = None
old_eye_right.parent = None
armature.data.edit_bones.remove(old_eye_left)
armature.data.edit_bones.remove(old_eye_right)
# Create new eye bones with original names
new_left_eye = armature.data.edit_bones.new(left_name)
new_right_eye = armature.data.edit_bones.new(right_name)
# Parent them
new_left_eye.parent = head
new_right_eye.parent = head
# Calculate straight up orientation matrix
straight_up_matrix = mathutils.Matrix.Rotation(math.pi/2, 3, 'X')
# Apply rotation while preserving position
for eye_data in [(new_left_eye, left_matrix, left_length),
(new_right_eye, right_matrix, right_length)]:
new_eye, orig_matrix, length = eye_data
new_matrix = straight_up_matrix.to_4x4()
new_matrix.translation = orig_matrix.translation
new_eye.matrix = new_matrix
new_eye.length = length
# Disable mirroring to prevent unwanted behavior
armature.data.use_mirror_x = False
progress.step("Finalizing setup")
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t('EyeTracking.success'))
return {'FINISHED'}
except Exception as e:
logger.error(f"Eye tracking setup failed: {str(e)}")
return {'CANCELLED'}
class CreateEyesSDK2Button(bpy.types.Operator):
"""Create eye tracking setup for VRChat SDK2"""
bl_idname = 'avatar_toolkit.create_eye_tracking_sdk2'
bl_label = t('EyeTracking.create.sdk2.label')
bl_description = t('EyeTracking.create.sdk2.desc')
bl_options = {'REGISTER', 'UNDO'}
mesh = None
@classmethod
def poll(cls, context):
if not get_all_meshes(context):
return False
toolkit = context.scene.avatar_toolkit
if not toolkit.head or not toolkit.eye_left or not toolkit.eye_right:
return False
if toolkit.disable_eye_blinking and toolkit.disable_eye_movement:
return False
return True
def execute(self, context):
toolkit = context.scene.avatar_toolkit
armature = get_active_armature(context)
with ProgressTracker(context, 100, "Creating SDK2 Eye Tracking") as progress:
# Validate setup
validator = EyeTrackingValidator()
is_valid, message = validator.validate_setup(context, toolkit.mesh_name_eye)
if not is_valid:
self.report({'ERROR'}, message)
return {'CANCELLED'}
try:
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
progress.step("Setting up bones")
self.mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
# Set up bones
head = armature.data.edit_bones.get(toolkit.head)
old_eye_left = armature.data.edit_bones.get(toolkit.eye_left)
old_eye_right = armature.data.edit_bones.get(toolkit.eye_right)
# Create new eye bones
new_left_eye = armature.data.edit_bones.new('LeftEye')
new_right_eye = armature.data.edit_bones.new('RightEye')
# Parent them
new_left_eye.parent = head
new_right_eye.parent = head
# Calculate positions for SDK2 style
fix_eye_position(context, old_eye_left, new_left_eye, head, False)
fix_eye_position(context, old_eye_right, new_right_eye, head, True)
progress.step("Processing vertex groups")
if not toolkit.disable_eye_movement:
# Switch to object mode for vertex group operations
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.mesh.select_set(True)
context.view_layer.objects.active = self.mesh
copy_vertex_group(self, old_eye_left.name, 'LeftEye')
copy_vertex_group(self, old_eye_right.name, 'RightEye')
# Return to armature edit mode
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
progress.step("Processing shape keys")
if not toolkit.disable_eye_blinking:
shapes = [toolkit.wink_left, toolkit.wink_right,
toolkit.lowerlid_left, toolkit.lowerlid_right]
new_shapes = ['vrc.blink_left', 'vrc.blink_right',
'vrc.lowerlid_left', 'vrc.lowerlid_right']
progress.step("Finalizing setup")
bpy.ops.object.mode_set(mode='OBJECT')
toolkit.eye_mode = 'TESTING'
self.report({'INFO'}, t('EyeTracking.success'))
return {'FINISHED'}
except Exception as e:
logger.error(f"Eye tracking setup failed: {str(e)}")
return {'CANCELLED'}
class EyeTrackingBackup:
def __init__(self):
self.backup_path = os.path.join(bpy.app.tempdir, "eye_tracking_backup.json")
self.bone_positions: Dict[str, Dict[str, Tuple[float, float, float]]] = {}
def store_bone_positions(self, armature) -> bool:
try:
self.bone_positions = {
'LeftEye': {
'head': tuple(armature.data.bones['LeftEye'].head_local),
'tail': tuple(armature.data.bones['LeftEye'].tail_local)
},
'RightEye': {
'head': tuple(armature.data.bones['RightEye'].head_local),
'tail': tuple(armature.data.bones['RightEye'].tail_local)
}
}
with open(self.backup_path, 'w') as f:
json.dump(self.bone_positions, f)
return True
except Exception as e:
logger.error(f"Backup failed: {str(e)}")
return False
def restore_bone_positions(self, armature) -> bool:
try:
if not os.path.exists(self.backup_path):
return False
with open(self.backup_path, 'r') as f:
backup_data = json.load(f)
bpy.ops.object.mode_set(mode='EDIT')
for bone_name, positions in backup_data.items():
if bone_name in armature.data.edit_bones:
bone = armature.data.edit_bones[bone_name]
bone.head = positions['head']
bone.tail = positions['tail']
return True
except Exception as e:
logger.error(f"Restore failed: {str(e)}")
return False
class EyeTrackingValidator:
@staticmethod
def find_eye_vertex_groups(mesh_name: str) -> Tuple[str, str]:
mesh = bpy.data.objects.get(mesh_name)
if not mesh:
return None, None
left_group = None
right_group = None
for group in mesh.vertex_groups:
if any(name.lower() in group.name.lower() for name in VALID_EYE_NAMES['left']):
left_group = group.name
if any(name.lower() in group.name.lower() for name in VALID_EYE_NAMES['right']):
right_group = group.name
return left_group, right_group
@staticmethod
def validate_setup(context, mesh_name: str) -> Tuple[bool, str]:
armature = get_active_armature(context)
if not armature:
return False, t('EyeTracking.validation.noArmature')
mesh = bpy.data.objects.get(mesh_name)
if not mesh:
return False, t('EyeTracking.validation.noMesh', mesh=mesh_name)
if not mesh.data.shape_keys:
return False, t('EyeTracking.validation.noShapekeys')
left_group, right_group = EyeTrackingValidator.find_eye_vertex_groups(mesh_name)
missing_groups = []
if not left_group:
missing_groups.append(t('EyeTracking.validation.leftEye'))
if not right_group:
missing_groups.append(t('EyeTracking.validation.rightEye'))
if missing_groups:
return False, t('EyeTracking.validation.missingGroups', groups=', '.join(missing_groups))
required_bones = [context.scene.avatar_toolkit.head,
context.scene.avatar_toolkit.eye_left,
context.scene.avatar_toolkit.eye_right]
missing_bones = [bone for bone in required_bones if bone not in armature.data.bones]
if missing_bones:
return False, t('EyeTracking.validation.missingBones', bones=', '.join(missing_bones))
return True, t('EyeTracking.validation.success')
class StartTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.start_eye_testing'
bl_label = t('EyeTracking.testing.start.label')
bl_description = t('EyeTracking.testing.start.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature and 'LeftEye' in armature.pose.bones and 'RightEye' in armature.pose.bones
def execute(self, context):
armature = get_active_armature(context)
bpy.ops.object.mode_set(mode='POSE')
armature.data.pose_position = 'POSE'
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
eye_left = armature.pose.bones.get('LeftEye')
eye_right = armature.pose.bones.get('RightEye')
eye_left_data = armature.data.bones.get('LeftEye')
eye_right_data = armature.data.bones.get('RightEye')
# Save initial rotations
eye_left.rotation_mode = 'XYZ'
eye_left_rot = copy.deepcopy(eye_left.rotation_euler)
eye_right.rotation_mode = 'XYZ'
eye_right_rot = copy.deepcopy(eye_right.rotation_euler)
if not all([eye_left, eye_right, eye_left_data, eye_right_data]):
return {'FINISHED'}
# Reset shape keys
mesh = bpy.data.objects[context.scene.avatar_toolkit.mesh_name_eye]
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = 0
# Clear transforms
for pb in armature.data.bones:
pb.select = True
bpy.ops.pose.transforms_clear()
for pb in armature.data.bones:
pb.select = False
pb.hide = True
eye_left_data.hide = False
eye_right_data.hide = False
context.scene.avatar_toolkit.eye_rotation_x = 0
context.scene.avatar_toolkit.eye_rotation_y = 0
return {'FINISHED'}
class StopTestingButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.stop_eye_testing'
bl_label = t('EyeTracking.testing.stop.label')
bl_description = t('EyeTracking.testing.stop.desc')
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
toolkit = context.scene.avatar_toolkit
if eye_left:
toolkit.eye_rotation_x = 0
toolkit.eye_rotation_y = 0
if not context.object or context.object.mode != 'POSE':
armature = get_active_armature(context)
bpy.ops.object.mode_set(mode='POSE')
armature = get_active_armature(context)
for pb in armature.data.bones:
pb.hide = False
pb.select = True
bpy.ops.pose.transforms_clear()
for pb in armature.data.bones:
pb.select = False
mesh = bpy.data.objects[toolkit.mesh_name_eye]
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = 0
eye_left = None
eye_right = None
eye_left_data = None
eye_right_data = None
eye_left_rot = []
eye_right_rot = []
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
def set_rotation(self, context):
global eye_left, eye_right, eye_left_rot, eye_right_rot
toolkit = context.scene.avatar_toolkit
if not eye_left or not eye_right:
StartTestingButton.execute(StartTestingButton, context)
return None
eye_left.rotation_mode = 'XYZ'
eye_right.rotation_mode = 'XYZ'
x_rotation = math.radians(toolkit.eye_rotation_x)
y_rotation = math.radians(toolkit.eye_rotation_y)
eye_left.rotation_euler[0] = eye_left_rot[0] + x_rotation
eye_left.rotation_euler[1] = eye_left_rot[1] + y_rotation
eye_right.rotation_euler[0] = eye_right_rot[0] + x_rotation
eye_right.rotation_euler[1] = eye_right_rot[1] + y_rotation
return None
class ResetRotationButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_eye_rotation'
bl_label = t('EyeTracking.reset.label')
bl_description = t('EyeTracking.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature and 'LeftEye' in armature.pose.bones and 'RightEye' in armature.pose.bones
def execute(self, context):
toolkit = context.scene.avatar_toolkit
armature = get_active_armature(context)
toolkit.eye_rotation_x = 0
toolkit.eye_rotation_y = 0
global eye_left, eye_right, eye_left_data, eye_right_data
eye_left = armature.pose.bones.get('LeftEye')
eye_right = armature.pose.bones.get('RightEye')
eye_left_data = armature.data.bones.get('LeftEye')
eye_right_data = armature.data.bones.get('RightEye')
for eye in [eye_left, eye_right]:
eye.rotation_mode = 'XYZ'
for i in range(3):
eye.rotation_euler[i] = 0
return {'FINISHED'}
class AdjustEyesButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_eyes'
bl_label = t('EyeTracking.adjust.label')
bl_description = t('EyeTracking.adjust.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature and all(bone in armature.pose.bones for bone in ['LeftEye', 'RightEye'])
def execute(self, context):
toolkit = context.scene.avatar_toolkit
if toolkit.disable_eye_movement:
return {'FINISHED'}
mesh_name = toolkit.mesh_name_eye
mesh = bpy.data.objects.get(mesh_name)
if not mesh:
self.report({'ERROR'}, t('EyeTracking.error.noMesh'))
return {'CANCELLED'}
for eye in ['LeftEye', 'RightEye']:
if not any(g.group == mesh.vertex_groups[eye].index for v in mesh.data.vertices for g in v.groups):
self.report({'ERROR'}, t('EyeTracking.error.noVertexGroup', bone=eye))
return {'CANCELLED'}
armature = get_active_armature(context)
bpy.ops.object.mode_set(mode='EDIT')
new_eye_left = armature.data.edit_bones.get('LeftEye')
new_eye_right = armature.data.edit_bones.get('RightEye')
old_eye_left = armature.pose.bones.get(toolkit.eye_left)
old_eye_right = armature.pose.bones.get(toolkit.eye_right)
fix_eye_position(context, old_eye_left, new_eye_left, None, False)
fix_eye_position(context, old_eye_right, new_eye_right, None, True)
bpy.ops.object.mode_set(mode='POSE')
global eye_left, eye_right, eye_left_data, eye_right_data
eye_left = armature.pose.bones.get('LeftEye')
eye_right = armature.pose.bones.get('RightEye')
eye_left_data = armature.data.bones.get('LeftEye')
eye_right_data = armature.data.bones.get('RightEye')
return {'FINISHED'}
class StartIrisHeightButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.adjust_iris_height'
bl_label = t('EyeTracking.iris.label')
bl_description = t('EyeTracking.iris.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature and all(bone in armature.pose.bones for bone in ['LeftEye', 'RightEye'])
def execute(self, context):
toolkit = context.scene.avatar_toolkit
if toolkit.disable_eye_movement:
return {'FINISHED'}
armature = get_active_armature(context)
armature.hide_viewport = True
mesh = bpy.data.objects[toolkit.mesh_name_eye]
mesh.select_set(True)
context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='EDIT')
if len(mesh.vertex_groups) > 0:
bpy.ops.mesh.select_mode(type='VERT')
for vg_name in ['LeftEye', 'RightEye']:
vg = mesh.vertex_groups.get(vg_name)
if vg:
bpy.ops.object.vertex_group_set_active(group=vg.name)
bpy.ops.object.vertex_group_select()
bm = bmesh.from_edit_mesh(mesh.data)
for v in bm.verts:
if v.select:
v.co.y += toolkit.iris_height * 0.01
logger.debug(f"Adjusted vertex position: {v.co}")
bmesh.update_edit_mesh(mesh.data)
return {'FINISHED'}
class TestBlinking(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_blinking'
bl_label = t('EyeTracking.blink.test.label')
bl_description = t('EyeTracking.blink.test.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
toolkit = context.scene.avatar_toolkit
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
return (mesh and mesh.data.shape_keys and
all(key in mesh.data.shape_keys.key_blocks for key in ['vrc.blink_left', 'vrc.blink_right']))
def execute(self, context):
toolkit = context.scene.avatar_toolkit
mesh = bpy.data.objects[toolkit.mesh_name_eye]
shapes = ['vrc.blink_left', 'vrc.blink_right']
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = toolkit.eye_blink_shape if shape_key.name in shapes else 0
return {'FINISHED'}
class TestLowerlid(bpy.types.Operator):
bl_idname = 'avatar_toolkit.test_lowerlid'
bl_label = t('EyeTracking.lowerlid.test.label')
bl_description = t('EyeTracking.lowerlid.test.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
toolkit = context.scene.avatar_toolkit
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
return (mesh and mesh.data.shape_keys and
all(key in mesh.data.shape_keys.key_blocks for key in ['vrc.lowerlid_left', 'vrc.lowerlid_right']))
def execute(self, context):
toolkit = context.scene.avatar_toolkit
mesh = bpy.data.objects[toolkit.mesh_name_eye]
shapes = OrderedDict()
shapes['vrc.lowerlid_left'] = toolkit.eye_lowerlid_shape
shapes['vrc.lowerlid_right'] = toolkit.eye_lowerlid_shape
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = toolkit.eye_lowerlid_shape if shape_key.name in shapes else 0
return {'FINISHED'}
class ResetBlinkTest(bpy.types.Operator):
bl_idname = 'avatar_toolkit.reset_blink_test'
bl_label = t('EyeTracking.blink.reset.label')
bl_description = t('EyeTracking.blink.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
toolkit = context.scene.avatar_toolkit
mesh = bpy.data.objects[toolkit.mesh_name_eye]
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = 0
toolkit.eye_blink_shape = 1
toolkit.eye_lowerlid_shape = 1
return {'FINISHED'}
def fix_eye_position(context, old_eye, new_eye, head, right_side):
toolkit = context.scene.avatar_toolkit
scale = -toolkit.eye_distance + 1
mesh = bpy.data.objects[toolkit.mesh_name_eye]
if not toolkit.disable_eye_movement:
if head:
coords_eye = find_center_vector_of_vertex_group(mesh, old_eye.name)
else:
coords_eye = find_center_vector_of_vertex_group(mesh, new_eye.name)
if coords_eye is False:
return
if head:
p1 = mesh.matrix_world @ head.head
p2 = mesh.matrix_world @ coords_eye
length = (p1 - p2).length
logger.debug(f"Eye distance: {length}")
x_cord, y_cord, z_cord = get_bone_orientations()
if toolkit.disable_eye_movement:
if head is not None:
new_eye.head[x_cord] = head.head[x_cord] + (0.05 if right_side else -0.05)
new_eye.head[y_cord] = head.head[y_cord]
new_eye.head[z_cord] = head.head[z_cord]
else:
new_eye.head[x_cord] = old_eye.head[x_cord] + scale * (coords_eye[0] - old_eye.head[x_cord])
new_eye.head[y_cord] = old_eye.head[y_cord] + scale * (coords_eye[1] - old_eye.head[y_cord])
new_eye.head[z_cord] = old_eye.head[z_cord] + scale * (coords_eye[2] - old_eye.head[z_cord])
new_eye.tail[x_cord] = new_eye.head[x_cord]
new_eye.tail[y_cord] = new_eye.head[y_cord]
new_eye.tail[z_cord] = new_eye.head[z_cord] + 0.1
def repair_shapekeys(mesh_name, vertex_group):
"""Fix VRC shape keys by slightly adjusting vertex positions"""
armature = get_active_armature(bpy.context)
mesh = bpy.data.objects[mesh_name]
mesh.select_set(True)
bpy.context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
bm = bmesh.new()
bm.from_mesh(mesh.data)
bm.verts.ensure_lookup_table()
logger.debug(f'Processing vertex group: {vertex_group}')
group = mesh.vertex_groups.get(vertex_group)
if group is None:
logger.warning(f'Group {vertex_group} not found, using fallback method')
repair_shapekeys_mouth(mesh_name)
return
vcoords = None
gi = group.index
for v in mesh.data.vertices:
for g in v.groups:
if g.group == gi:
vcoords = v.co.xyz
if not vcoords:
return
logger.info('Repairing shape keys')
moved = False
i = 0
for key in bm.verts.layers.shape.keys():
if not key.startswith('vrc.'):
continue
logger.debug(f'Repairing shape: {key}')
value = bm.verts.layers.shape.get(key)
for index, vert in enumerate(bm.verts):
if vert.co.xyz == vcoords:
if index < i:
continue
shapekey = vert
shapekey_coords = mesh.matrix_world @ shapekey[value]
shapekey_coords[0] -= 0.00007 * randBoolNumber()
shapekey_coords[1] -= 0.00007 * randBoolNumber()
shapekey_coords[2] -= 0.00007 * randBoolNumber()
shapekey[value] = mesh.matrix_world.inverted() @ shapekey_coords
logger.debug(f'Repaired shape: {key}')
i += 1
moved = True
break
bm.to_mesh(mesh.data)
if not moved:
logger.warning('Shape key repair failed, using random method')
repair_shapekeys_mouth(mesh_name)
def randBoolNumber():
return -1 if random() < 0.5 else 1
def repair_shapekeys_mouth(mesh_name):
mesh = bpy.data.objects[mesh_name]
mesh.select_set(True)
bpy.context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
bm = bmesh.new()
bm.from_mesh(mesh.data)
bm.verts.ensure_lookup_table()
moved = False
for key in bm.verts.layers.shape.keys():
if not key.startswith('vrc'):
continue
value = bm.verts.layers.shape.get(key)
for vert in bm.verts:
shapekey = vert
shapekey_coords = mesh.matrix_world @ shapekey[value]
shapekey_coords[0] -= 0.00007
shapekey_coords[1] -= 0.00007
shapekey_coords[2] -= 0.00007
shapekey[value] = mesh.matrix_world.inverted() @ shapekey_coords
moved = True
break
bm.to_mesh(mesh.data)
if not moved:
logger.error('Random shape key repair failed')
def get_bone_orientations():
"""Get bone orientation axes"""
return (0, 1, 2) # x, y, z coordinates
def find_center_vector_of_vertex_group(mesh, group_name):
"""Calculate center position of vertex group"""
group = mesh.vertex_groups.get(group_name)
if not group:
return False
vertices = []
for vert in mesh.data.vertices:
for g in vert.groups:
if g.group == group.index:
vertices.append(vert.co)
if not vertices:
return False
return sum((v for v in vertices), mathutils.Vector()) / len(vertices)
def vertex_group_exists(mesh_obj, group_name):
"""Check if vertex group exists and has weights"""
if not mesh_obj or group_name not in mesh_obj.vertex_groups:
return False
group = mesh_obj.vertex_groups[group_name]
for vert in mesh_obj.data.vertices:
for g in vert.groups:
if g.group == group.index and g.weight > 0:
return True
return False
def copy_vertex_group(self, vertex_group, rename_to):
"""Copy vertex group with new name"""
vertex_group_index = 0
# Select and make mesh active
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.mesh.select_set(True)
bpy.context.view_layer.objects.active = self.mesh
for group in self.mesh.vertex_groups:
if group.name == vertex_group:
self.mesh.vertex_groups.active_index = vertex_group_index
bpy.ops.object.vertex_group_copy()
self.mesh.vertex_groups[vertex_group + '_copy'].name = rename_to
break
vertex_group_index += 1
def copy_shape_key(self, context, from_shape, new_names, new_index):
"""Copy shape key with new name"""
blinking = not context.scene.avatar_toolkit.disable_eye_blinking
new_name = new_names[new_index - 1]
# Rename existing shapekey if it exists
for shapekey in self.mesh.data.shape_keys.key_blocks:
shapekey.value = 0
if shapekey.name == new_name:
shapekey.name = shapekey.name + '_old'
if from_shape == new_name:
from_shape = shapekey.name
# Create new shape key
for index, shapekey in enumerate(self.mesh.data.shape_keys.key_blocks):
if from_shape == shapekey.name:
self.mesh.active_shape_key_index = index
shapekey.value = 1
self.mesh.shape_key_add(name=new_name, from_mix=blinking)
break
# Reset shape keys
for shapekey in self.mesh.data.shape_keys.key_blocks:
shapekey.value = 0
self.mesh.active_shape_key_index = 0
return from_shape
# Global state for eye tracking
eye_left = None
eye_right = None
eye_left_data = None
eye_right_data = None
eye_left_rot = []
eye_right_rot = []
class VertexGroupCache:
"""Cache for vertex group operations"""
_cache = {}
@classmethod
def get_vertex_indices(cls, mesh_name: str, group_name: str) -> Optional[set]:
cache_key = f"{mesh_name}_{group_name}"
if cache_key in cls._cache:
return cls._cache[cache_key]
mesh = bpy.data.objects.get(mesh_name)
if not mesh:
return None
group = mesh.vertex_groups.get(group_name)
if not group:
return None
indices = {v.index for v in mesh.data.vertices
if any(g.group == group.index for g in v.groups)}
cls._cache[cache_key] = indices
return indices
@classmethod
def clear_cache(cls):
cls._cache.clear()
class RotateEyeBonesForAv3Button(Operator):
"""Reorient eye bones for proper VRChat eye tracking"""
bl_idname = "avatar_toolkit.rotate_eye_bones"
bl_label = t("EyeTracking.rotate.label")
bl_description = t("EyeTracking.rotate.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature and all(bone in armature.pose.bones for bone in ['LeftEye', 'RightEye'])
def execute(self, context):
armature = get_active_armature(context)
straight_up_matrix = mathutils.Matrix.Rotation(math.pi/2, 3, 'X')
bpy.ops.object.mode_set(mode='EDIT')
for eye_name in ['LeftEye', 'RightEye']:
eye_bone = armature.data.edit_bones[eye_name]
new_matrix = straight_up_matrix.to_4x4()
new_matrix.translation = eye_bone.matrix.translation
eye_bone.matrix = new_matrix
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
class ResetEyeTrackingButton(Operator):
"""Reset all eye tracking settings and state"""
bl_idname = 'avatar_toolkit.reset_eye_tracking'
bl_label = t('EyeTracking.reset.label')
bl_description = t('EyeTracking.reset.desc')
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
eye_left = eye_right = eye_left_data = eye_right_data = None
eye_left_rot = eye_right_rot = []
context.scene.avatar_toolkit.eye_mode = 'CREATION'
return {'FINISHED'}
def validate_weights(mesh_obj: Object, vertex_group: str) -> bool:
"""Validate vertex group weights"""
group = mesh_obj.vertex_groups.get(vertex_group)
if not group:
return False
for vertex in mesh_obj.data.vertices:
for group_element in vertex.groups:
if group_element.group == group.index and group_element.weight > 0:
return True
return False
def get_eye_bone_names(armature: Object) -> Dict[str, str]:
"""Get standardized eye bone names"""
eye_bones = {'left': None, 'right': None}
for bone in armature.data.bones:
if any(name.lower() in bone.name.lower() for name in VALID_EYE_NAMES['left']):
eye_bones['left'] = bone.name
if any(name.lower() in bone.name.lower() for name in VALID_EYE_NAMES['right']):
eye_bones['right'] = bone.name
return eye_bones
def stop_testing(context: Context) -> None:
"""Stop eye tracking testing mode"""
global eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot
if not all([eye_left, eye_right, eye_left_data, eye_right_data, eye_left_rot, eye_right_rot]):
return
armature = get_active_armature(context)
if not armature:
return
bpy.ops.object.mode_set(mode='POSE')
# Reset rotations
context.scene.avatar_toolkit.eye_rotation_x = 0
context.scene.avatar_toolkit.eye_rotation_y = 0
# Clear transforms
for bone in armature.data.bones:
bone.hide = False
bone.select = True
bpy.ops.pose.transforms_clear()
# Reset shape keys
mesh = bpy.data.objects.get(context.scene.avatar_toolkit.mesh_name_eye)
if mesh and mesh.data.shape_keys:
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = 0
# Clear globals
eye_left = eye_right = eye_left_data = eye_right_data = None
eye_left_rot = eye_right_rot = []
-189
View File
@@ -1,189 +0,0 @@
import bpy
from bpy.types import Operator
from bpy_extras.io_utils import ImportHelper
from ..core.register import register_wrap
from ..core.importer import imports, import_types
from ..core.common import remove_default_objects
from ..functions.translations import t
import pathlib
import os
VRM_IMPORTER_URL = "https://github.com/saturday06/VRM_Addon_for_Blender"
@register_wrap
class AvatarToolKit_OT_ImportAnyModel(Operator, ImportHelper):
bl_idname = 'avatar_toolkit.import_any_model'
bl_label = t('Tools.import_any_model.label')
bl_description = t('Tools.import_any_model.desc')
bl_options = {'REGISTER', 'UNDO'}
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
filter_glob: bpy.props.StringProperty(default=imports, options={'HIDDEN', 'SKIP_SAVE'})
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
# since I wrote this myself, a bit more efficient than cats. mostly - @989onan
def execute(self, context: bpy.types.Context):
file_grouping_dict: dict[str, list[dict[str, str]]] = dict() # group our files so our importers can import them together. in the case of OBJ+MTL and others that need grouped files, this is extremely important.
remove_default_objects()
# check if we are importing multiple files
is_multi = len(self.files) > 0
if is_multi:
for file in self.files:
fullpath = os.path.join(self.directory, os.path.basename(file.name))
name = pathlib.Path(fullpath).suffix.replace(".", "")
# this makes sure our imports that should be grouped stay together.
# basically the method checks for if the first value has a lambda with the same bytecode as another lambda, then it will use that value's key (ex:"obj"<->"mtl" or "fbx"), keeping same importers together
if name not in file_grouping_dict:
file_grouping_dict[name] = []
file_grouping_dict[name].append({"name": os.path.basename(file.name)}) # emulate passing a list of files.
else:
fullpath: str = os.path.join(os.path.dirname(self.filepath), os.path.basename(self.filepath))
name = pathlib.Path(fullpath).suffix.replace(".", "")
if name not in file_grouping_dict:
file_grouping_dict[name] = []
file_grouping_dict[name].append({"name": fullpath}) # emulate passing a list of files.
# import the files together to make sure things like obj import together. This is important
for file_group_name, files in file_grouping_dict.items():
try:
# Check for VRM importer availability
if file_group_name == "vrm" and not hasattr(bpy.ops.import_scene, "vrm"):
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
return {'CANCELLED'}
if self.directory:
import_types[file_group_name](self.directory, files, self.filepath)
else:
import_types[file_group_name]("", files, self.filepath) # give an empty directory, works just fine for 90%
except AttributeError as e:
if file_group_name == "vrm":
bpy.ops.wm.vrm_importer_popup('INVOKE_DEFAULT')
else:
self.report({'ERROR'}, t('Importing.need_importer').format(extension=file_group_name))
print("Importer error:", e)
return {'CANCELLED'}
self.report({'INFO'}, t('Quick_Access.import_success'))
return {'FINISHED'}
@register_wrap
class VRMImporterPopup(Operator):
bl_idname = "wm.vrm_importer_popup"
bl_label = "VRM Importer Not Installed"
def execute(self, context):
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=300)
def draw(self, context):
layout = self.layout
layout.label(text="VRM importer plugin is not installed.")
layout.label(text="Please install it to import VRM files.")
layout.operator("wm.url_open", text="Get VRM Importer").url = VRM_IMPORTER_URL
#TODO: This needs to be done with our own MMD importer.
"""
#stolen from cats. Oh wait I made this code riiiiiiight - @989onan
@register_wrap
class ImportMMDAnimation(bpy.types.Operator, ImportHelper):
bl_idname = 'avatar_toolkit.import_mmd_animation'
bl_label = t('Importer.mmd_anim_importer.label')
bl_description = t('Importer.mmd_anim_importer.desc')
bl_options = {'INTERNAL', 'UNDO'}
filter_glob: bpy.props.StringProperty(
default="*.vmd",
options={'HIDDEN'}
)
files: bpy.props.CollectionProperty(type=bpy.types.OperatorFileListElement, options={'HIDDEN', 'SKIP_SAVE'})
directory: bpy.props.StringProperty(maxlen=1024, subtype='FILE_PATH', options={'HIDDEN', 'SKIP_SAVE'})
filepath: bpy.props.StringProperty()
@classmethod
def poll(cls, context):
if common.get_armature(context) is None:
return False
return True
def execute(self, context):
# Make sure that the first layer is visible
if hasattr(context.scene, 'layers'):
context.scene.layers[0] = True
filename, extension = os.path.splitext(self.filepath)
if(extension == ".vmd"):
#A dictionary to change the current model to MMD importer compatable temporarily
bonedict = {
"chest":"UpperBody",
"neck":"Neck",
"head":"Head",
"hips":"Center",
"spine":"LowerBody",
"right_wrist":"Wrist_R",
"right_elbow":"Elbow_R",
"right_arm":"Arm_R",
"right_shoulder":"Shoulder_R",
"right_leg":"Leg_R",
"right_knee":"Knee_R",
"right_ankle":"Ankle_R",
"right_toe":"Toe_R",
"left_wrist":"Wrist_L",
"left_elbow":"Elbow_L",
"left_arm":"Arm_L",
"left_shoulder":"Shoulder_L",
"left_leg":"Leg_L",
"left_knee":"Knee_L",
"left_ankle":"Ankle_L",
"left_toe":"Toe_L"
}
armature = common.get_armature(context)
common.unselect_all()
common.Set_Mode(context, 'OBJECT')
common.unselect_all()
common.set_active(armature)
orig_names = dict()
reverse_bone_lookup = dict()
for (preferred_name, name_list) in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
for bone in armature.data.bones:
if common.simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[common.simplify_bonename(bone.name)] in bonedict:
orig_names[bonedict[reverse_bone_lookup[common.simplify_bonename(bone.name)]]] = bone.name
bone.name = bonedict[reverse_bone_lookup[common.simplify_bonename(bone.name)]]
try:
bpy.ops.mmd_tools.import_vmd(filepath=self.filepath,bone_mapper='RENAMED_BONES',use_underscore=True, dictionary='INTERNAL')
except AttributeError as e:
print("importer error was:")
print(e)
print(t('Importing.importer_search_term'))
common.open_web_after_delay_multi_threaded(delay=12, url=t('Importing.importer_search_term').format(extension = "MMD"))
self.report({'ERROR'},t('Importing.need_importer').format(extension = "MMD"))
return {'CANCELLED'}
#iterate through bones and put them back, therefore blender API will change the animation to be correct.
#this is because renaming bones fixes the animation targets in the data model.
for bone in armature.data.bones:
if common.simplify_bonename(bone.name) in orig_names:
bone.name = orig_names[common.simplify_bonename(bone.name)]
common.unselect_all()
common.Set_Mode(context, 'OBJECT')
common.unselect_all()
common.set_active(armature)
return {'FINISHED'} """
-211
View File
@@ -1,211 +0,0 @@
import numpy as np
import bpy
from typing import List, Optional, Set
from bpy.types import Operator, Context, Object
from ..core.common import fix_uv_coordinates, get_selected_armature, get_all_meshes, is_valid_armature, apply_shapekey_to_basis, has_shapekeys, select_current_armature, init_progress, update_progress, finish_progress
from ..functions.translations import t
from ..core.register import register_wrap
@register_wrap
class AvatarToolkit_OT_RemoveUnusedShapekeys(bpy.types.Operator):
tolerance: bpy.props.FloatProperty(name=t("Tools.remove_unused_shapekeys.tolerance.label"), default=0.001, description=t("Tools.remove_unused_shapekeys.tolerance.desc"))
bl_idname = "avatar_toolkit.remove_unused_shapekeys"
bl_label = t("Tools.remove_unused_shapekeys.label")
bl_description = t("Tools.remove_unused_shapekeys.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT")
def execute(self, context: Context) -> set[str]:
#Shamefully taken from: https://blender.stackexchange.com/a/237611
#at least I am crediting them - @989onan
for ob in get_all_meshes(context):
if not ob.data.shape_keys: continue
if not ob.data.shape_keys.use_relative: continue
kbs = ob.data.shape_keys.key_blocks
nverts = len(ob.data.vertices)
to_delete = []
# Cache locs for rel keys since many keys have the same rel key
cache = {}
locs = np.empty(3*nverts, dtype=np.float32)
for kb in kbs:
if kb == kb.relative_key: continue
kb.data.foreach_get("co", locs)
if kb.relative_key.name not in cache:
rel_locs = np.empty(3*nverts, dtype=np.float32)
kb.relative_key.data.foreach_get("co", rel_locs)
cache[kb.relative_key.name] = rel_locs
rel_locs = cache[kb.relative_key.name]
locs -= rel_locs
if (np.abs(locs) < self.tolerance).all():
to_delete.append(kb.name)
for kb_name in to_delete:
if ("-" in kb_name) or ("=" in kb_name) or ("~" in kb_name): #don't delete category names. - @989onan
continue
ob.shape_key_remove(ob.data.shape_keys.key_blocks[kb_name])
@register_wrap
class AvatarToolkit_OT_ApplyShapeKey(bpy.types.Operator):
bl_idname = "avatar_toolkit.apply_shape_key"
bl_label = t("Tools.apply_shape_key.label")
bl_description = t("Tools.apply_shape_key.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and (len(get_all_meshes(context)) > 0) and (context.mode == "OBJECT") and context.view_layer.objects.active is not None and has_shapekeys(context.view_layer.objects.active)
def execute(self, context: Context) -> set[str]:
obj: bpy.types.Object = context.view_layer.objects.active
if (apply_shapekey_to_basis(context,obj,obj.active_shape_key.name,False)):
return {'FINISHED'}
else:
self.report({'ERROR'}, t("Tools.apply_shape_key.error"))
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_JoinAllMeshes(Operator):
bl_idname = "avatar_toolkit.join_all_meshes"
bl_label = t("Optimization.join_all_meshes.label")
bl_description = t("Optimization.join_all_meshes.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> Set[str]:
try:
self.join_all_meshes(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"{t('Optimization.join_error')}: {str(e)}")
return {'CANCELLED'}
def join_all_meshes(self, context: Context) -> None:
if not select_current_armature(context):
raise ValueError(t("Optimization.no_armature_selected"))
armature = get_selected_armature(context)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
meshes: List[Object] = get_all_meshes(context)
if not meshes:
raise ValueError(t("Optimization.no_meshes_found"))
init_progress(context, 5) # 5 steps in total
update_progress(self, context, t("Optimization.selecting_meshes"))
for mesh in meshes:
mesh.select_set(True)
if bpy.context.selected_objects:
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
update_progress(self, context, t("Optimization.joining_meshes"))
try:
bpy.ops.object.join()
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.applying_transforms"))
try:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
bpy.ops.object.mode_set(mode='OBJECT')
fix_uv_coordinates(context)
update_progress(self, context, t("Optimization.finalizing"))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.report({'INFO'}, t("Optimization.meshes_joined"))
else:
raise ValueError(t("Optimization.no_mesh_selected"))
context.view_layer.objects.active = armature
finish_progress(context)
@register_wrap
class AvatarToolKit_OT_JoinSelectedMeshes(Operator):
bl_idname = "avatar_toolkit.join_selected_meshes"
bl_label = t("Optimization.join_selected_meshes.label")
bl_description = t("Optimization.join_selected_meshes.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return context.mode == 'OBJECT' and len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1
def execute(self, context: Context) -> Set[str]:
try:
self.join_selected_meshes(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, f"{t('Optimization.join_error')}: {str(e)}")
return {'CANCELLED'}
def join_selected_meshes(self, context: Context) -> None:
selected_objects: List[Object] = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']
if len(selected_objects) < 2:
raise ValueError(t("Optimization.select_at_least_two_meshes"))
init_progress(context, 5) # 5 steps in total
update_progress(self, context, t("Optimization.preparing_meshes"))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
update_progress(self, context, t("Optimization.selecting_meshes"))
for obj in selected_objects:
obj.select_set(True)
if bpy.context.selected_objects:
bpy.context.view_layer.objects.active = bpy.context.selected_objects[0]
update_progress(self, context, t("Optimization.joining_meshes"))
try:
bpy.ops.object.join()
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.join_operation_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.applying_transforms"))
try:
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
except RuntimeError as e:
raise RuntimeError(f"{t('Optimization.transform_apply_failed')}: {str(e)}")
update_progress(self, context, t("Optimization.fixing_uv_coordinates"))
fix_uv_coordinates(context)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
self.report({'INFO'}, t("Optimization.selected_meshes_joined"))
else:
raise ValueError(t("Optimization.no_mesh_selected"))
finish_progress(context)
-398
View File
@@ -1,398 +0,0 @@
import bpy
import numpy as np
import re
from bpy.types import Operator, Context, Material, ShaderNodeTexImage, ShaderNodeGroup, Object
from ..core.register import register_wrap
from ..functions.translations import t
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
from ..functions.additional_tools import AvatarToolKit_OT_ConnectBones, AvatarToolKit_OT_DeleteBoneConstraints
from ..functions.armature_modifying import AvatarToolkit_OT_RemoveZeroWeightBones, AvatarToolkit_OT_MergeBonesToParents
@register_wrap
class AvatarToolKit_OT_CleanupMesh(Operator):
bl_idname = "avatar_toolkit.cleanup_mesh"
bl_label = t("MMDOptions.cleanup_mesh.label")
bl_description = t("MMDOptions.cleanup_mesh.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
init_progress(context, 4)
update_progress(self, context, t("MMDOptions.removing_empty_objects"))
bpy.ops.object.select_all(action='DESELECT')
for obj in context.scene.objects:
if obj.type == 'EMPTY':
obj.select_set(True)
bpy.ops.object.delete()
update_progress(self, context, t("MMDOptions.removing_unused_vertex_groups"))
for obj in get_all_meshes(context):
self.remove_unused_vertex_groups(obj)
update_progress(self, context, t("MMDOptions.removing_unused_vertices"))
for obj in get_all_meshes(context):
self.remove_unused_vertices(obj)
update_progress(self, context, t("MMDOptions.removing_empty_shape_keys"))
for obj in get_all_meshes(context):
self.remove_empty_shape_keys(obj)
finish_progress(context)
return {'FINISHED'}
def remove_unused_vertex_groups(self, obj):
vgroups = obj.vertex_groups
for vgroup in vgroups:
if not any(vgroup.index in [g.group for g in v.groups] for v in obj.data.vertices):
vgroups.remove(vgroup)
def remove_unused_vertices(self, obj):
bpy.ops.object.select_all(action='DESELECT')
obj.select_set(True)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.delete_loose()
bpy.ops.object.mode_set(mode='OBJECT')
def remove_empty_shape_keys(self, obj):
if obj.data.shape_keys:
for key in obj.data.shape_keys.key_blocks:
if key.name != 'Basis' and all(abs(key.data[i].co[j] - obj.data.shape_keys.reference_key.data[i].co[j]) < 0.0001 for i in range(len(key.data)) for j in range(3)):
obj.shape_key_remove(key)
@register_wrap
class AvatarToolKit_OT_OptimizeWeights(Operator):
bl_idname = "avatar_toolkit.optimize_weights"
bl_label = t("MMDOptions.optimize_weights.label")
bl_description = t("MMDOptions.optimize_weights.desc")
bl_options = {'REGISTER', 'UNDO'}
max_weights: bpy.props.IntProperty(
name=t("MMDOptions.max_weights.label"),
description=t("MMDOptions.max_weights.desc"),
default=4,
min=1,
max=8
)
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'ERROR'}, t("MMDOptions.no_armature_selected"))
return {'CANCELLED'}
init_progress(context, 4)
update_progress(self, context, t("MMDOptions.merging_weights"))
for obj in get_all_meshes(context):
for modifier in obj.modifiers:
if modifier.type == 'ARMATURE' and modifier.object != armature:
bpy.ops.object.modifier_apply(modifier=modifier.name)
update_progress(self, context, t("MMDOptions.removing_zero_weight_bones"))
bpy.ops.avatar_toolkit.remove_zero_weight_bones('EXEC_DEFAULT')
update_progress(self, context, t("MMDOptions.limiting_vertex_weights"))
for obj in get_all_meshes(context):
self.limit_vertex_weights(obj)
update_progress(self, context, t("MMDOptions.weight_optimization_complete"))
finish_progress(context)
return {'FINISHED'}
def limit_vertex_weights(self, obj):
for v in obj.data.vertices:
if len(v.groups) > self.max_weights:
sorted_groups = sorted(v.groups, key=lambda g: g.weight, reverse=True)
for g in sorted_groups[self.max_weights:]:
obj.vertex_groups[g.group].remove([v.index])
@register_wrap
class AvatarToolKit_OT_OptimizeArmature(Operator):
bl_idname = "avatar_toolkit.optimize_armature"
bl_label = t("MMDOptions.optimize_armature.label")
bl_description = t("MMDOptions.optimize_armature.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'ERROR'}, t("MMDOptions.no_armature_selected"))
return {'CANCELLED'}
init_progress(context, 9)
# Ensure proper object selection and mode
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
# Store initial transforms
bpy.ops.object.mode_set(mode='EDIT')
initial_transforms = {}
for bone in armature.data.edit_bones:
initial_transforms[bone.name] = {
'head': bone.head.copy(),
'tail': bone.tail.copy(),
'roll': bone.roll,
'matrix': bone.matrix.copy(),
'parent': bone.parent.name if bone.parent else None
}
update_progress(self, context, t("MMDOptions.deleting_bone_constraints"))
bpy.ops.avatar_toolkit.delete_bone_constraints('EXEC_DEFAULT')
update_progress(self, context, t("MMDOptions.merging_bones_to_parents"))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
try:
bpy.ops.avatar_toolkit.merge_bones_to_parents('EXEC_DEFAULT')
except RuntimeError as e:
self.report({'WARNING'}, f"Failed to merge bones to parents: {str(e)}")
update_progress(self, context, t("MMDOptions.reordering_bones"))
self.reorder_bones(context, armature)
update_progress(self, context, t("MMDOptions.fixing_armature_names"))
self.fix_armature_names(armature)
update_progress(self, context, t("MMDOptions.renaming_bones"))
self.rename_bones(armature)
# Restore original bone transforms
bpy.ops.object.mode_set(mode='EDIT')
for bone_name, transform in initial_transforms.items():
if bone_name in armature.data.edit_bones:
bone = armature.data.edit_bones[bone_name]
bone.head = transform['head']
bone.tail = transform['tail']
bone.roll = transform['roll']
bone.matrix = transform['matrix']
update_progress(self, context, t("MMDOptions.armature_optimization_complete"))
# Ensure we end in object mode with proper selection
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
finish_progress(context)
return {'FINISHED'}
def reorder_bones(self, context: Context, armature: bpy.types.Object):
def sort_bones(bone):
children = sorted(bone.children, key=lambda b: b.name)
for child in children:
sort_bones(child)
bpy.ops.object.mode_set(mode='EDIT')
root_bones = [bone for bone in armature.data.edit_bones if not bone.parent]
for root_bone in sorted(root_bones, key=lambda b: b.name):
sort_bones(root_bone)
def fix_armature_names(self, armature):
for bone in armature.data.bones:
fixed_name = self.get_fixed_bone_name(bone.name)
if fixed_name != bone.name:
bone.name = fixed_name
def get_fixed_bone_name(self, name):
name = name.replace(' ', '_')
name = re.sub(r'[^\w]', '', name)
return name
def rename_bones(self, armature):
for bone in armature.data.bones:
new_name = self.get_standardized_bone_name(bone.name)
if new_name != bone.name:
bone.name = new_name
def get_standardized_bone_name(self, name):
if 'left' in name.lower():
return f"Left_{name}"
elif 'right' in name.lower():
return f"Right_{name}"
return name
def bake_mmd_colors(node_base_tex: ShaderNodeTexImage, node_mmd_shader: ShaderNodeGroup):
ambient_color_input = node_mmd_shader.inputs.get("Ambient Color")
diffuse_color_input = node_mmd_shader.inputs.get("Diffuse Color")
if not ambient_color_input or not diffuse_color_input:
return node_base_tex, None
ambient_color = np.array(ambient_color_input.default_value[:3])
diffuse_color = np.array(diffuse_color_input.default_value[:3])
mmd_color = np.clip(ambient_color + diffuse_color * 0.6, 0, 1)
if not node_base_tex or not node_base_tex.image:
principled_base_color = np.append(mmd_color, 1)
return None, principled_base_color
base_tex_image = node_base_tex.image
if not base_tex_image.pixels:
return node_base_tex, None
if base_tex_image.colorspace_settings.name == 'sRGB':
is_small_mask = mmd_color < 0.0031308
mmd_color[is_small_mask] = np.where(mmd_color[is_small_mask] < 0.0, 0, mmd_color[is_small_mask] * 12.92)
is_large_mask = np.invert(is_small_mask)
mmd_color[is_large_mask] = (mmd_color[is_large_mask] ** (1.0 / 2.4)) * 1.055 - 0.055
pixels = np.array(base_tex_image.pixels).reshape((-1, 4))
pixels[:, :3] *= mmd_color
baked_image = bpy.data.images.new(base_tex_image.name + "MMDCatsBaked",
width=base_tex_image.size[0],
height=base_tex_image.size[1],
alpha=True)
baked_image.filepath = bpy.path.abspath("//" + base_tex_image.name + ".png")
baked_image.file_format = 'PNG'
baked_image.colorspace_settings.name = base_tex_image.colorspace_settings.name
baked_image.pixels = pixels.flatten()
node_base_tex.image = baked_image
if bpy.data.is_saved:
baked_image.save()
return node_base_tex, None
def add_principled_shader(material: Material, bake_mmd=True):
node_tree = material.node_tree
nodes = node_tree.nodes
links = node_tree.links
principled_shader = nodes.new(type="ShaderNodeBsdfPrincipled")
principled_shader.label = "Cats Export Shader"
principled_shader.location = (501, -500)
output_shader = nodes.new(type="ShaderNodeOutputMaterial")
output_shader.label = "Cats Export"
output_shader.location = (801, -500)
links.new(principled_shader.outputs["BSDF"], output_shader.inputs["Surface"])
node_base_tex = nodes.get("mmd_base_tex") or next((n for n in nodes if n.type == 'TEX_IMAGE'), None)
node_mmd_shader = nodes.get("mmd_shader")
if node_mmd_shader and bake_mmd:
node_base_tex, principled_base_color = bake_mmd_colors(node_base_tex, node_mmd_shader)
else:
principled_base_color = None
if node_base_tex and node_base_tex.image:
links.new(node_base_tex.outputs["Color"], principled_shader.inputs["Base Color"])
links.new(node_base_tex.outputs["Alpha"], principled_shader.inputs["Alpha"])
elif principled_base_color is not None:
principled_shader.inputs["Base Color"].default_value = principled_base_color
principled_shader.inputs["Specular IOR Level"].default_value = 0
principled_shader.inputs["Roughness"].default_value = 0.9
principled_shader.inputs["Sheen Tint"].default_value = (1.0, 1.0, 1.0, 1.0)
principled_shader.inputs["Coat Roughness"].default_value = 0
principled_shader.inputs["IOR"].default_value = 1.45
# Handle transparency
if material.blend_method != 'OPAQUE':
principled_shader.inputs["Alpha"].default_value = material.alpha_threshold
material.blend_method = 'CLIP'
material.shadow_method = 'CLIP'
def fix_mmd_shader(material: Material):
mmd_shader_node = material.node_tree.nodes.get("mmd_shader")
if mmd_shader_node:
reflect_input = mmd_shader_node.inputs.get("Reflect")
if reflect_input:
reflect_input.default_value = 1
def fix_vrm_shader(material: Material):
nodes = material.node_tree.nodes
is_vrm_mat = False
for node in nodes:
if hasattr(node, 'node_tree') and 'MToon_unversioned' in node.node_tree.name:
node.location[0] = 200
node.inputs['ReceiveShadow_Texture_alpha'].default_value = -10000
node.inputs['ShadeTexture'].default_value = (1.0, 1.0, 1.0, 1.0)
node.inputs['Emission_Texture'].default_value = (0.0, 0.0, 0.0, 0.0)
node.inputs['SphereAddTexture'].default_value = (0.0, 0.0, 0.0, 0.0)
node_input = node.inputs.get('NomalmapTexture') or node.inputs.get('NormalmapTexture')
node_input.default_value = (1.0, 1.0, 1.0, 1.0)
is_vrm_mat = True
break
if is_vrm_mat:
nodes_to_keep = ['DiffuseColor', 'MainTexture', 'Emission_Texture']
if 'HAIR' in material.name:
nodes_to_keep.append('SphereAddTexture')
for node in nodes:
if ('RGB' in node.name or 'Value' in node.name or 'Image Texture' in node.name or
'UV Map' in node.name or 'Mapping' in node.name):
if node.label not in nodes_to_keep:
material.node_tree.links = [link for link in material.node_tree.links
if not (link.from_node == node or link.to_node == node)]
@register_wrap
class AvatarToolKit_OT_ConvertMaterials(Operator):
bl_idname = "avatar_toolkit.convert_materials"
bl_label = t("MMDOptions.convert_materials.label")
bl_description = t("MMDOptions.convert_materials.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> set[str]:
meshes = get_all_meshes(context)
init_progress(context, len(meshes))
for obj in meshes:
update_progress(self, context, t("MMDOptions.converting_materials").format(name=obj.name))
self.convert_materials_for_mesh(obj)
finish_progress(context)
return {'FINISHED'}
def convert_materials_for_mesh(self, mesh: Object):
for mat_slot in mesh.material_slots:
if mat_slot.material:
mat = mat_slot.material
mat.use_nodes = True
# Add Principled BSDF shader
add_principled_shader(mat)
# Fix MMD shader if present
fix_mmd_shader(mat)
# Fix VRM shader if present
fix_vrm_shader(mat)
# Clean up unused nodes
self.clean_unused_nodes(mat)
def clean_unused_nodes(self, material: Material):
nodes = material.node_tree.nodes
links = material.node_tree.links
used_nodes = set()
output_node = next((n for n in nodes if n.type == 'OUTPUT_MATERIAL'), None)
if output_node:
self.traverse_node_tree(output_node, used_nodes)
for node in nodes:
if node not in used_nodes:
nodes.remove(node)
def traverse_node_tree(self, node, used_nodes):
used_nodes.add(node)
for input in node.inputs:
for link in input.links:
if link.from_node not in used_nodes:
self.traverse_node_tree(link.from_node, used_nodes)
+792
View File
@@ -0,0 +1,792 @@
import bpy
from mathutils import Vector
from typing import Dict, List, Tuple, Set, Optional
from bpy.types import Object, Armature, EditBone, Bone, Operator, Context
from ..core.logging_setup import logger
from ..core.common import (
ProgressTracker,
get_active_armature,
validate_armature,
get_vertex_weights,
transfer_vertex_weights,
get_all_meshes
)
from ..core.translations import t
from ..core.dictionaries import bone_names, dont_delete_these_main_bones
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
"""MMD Bone standardization system"""
bl_idname = "avatar_toolkit.standardize_mmd"
bl_label = t("MMD.standardize")
bl_options = {'REGISTER', 'UNDO'}
def __init__(self):
self.bone_mapping: Dict[str, str] = {}
self.processed_bones: Set[str] = set()
def execute(self, context: Context) -> Set[str]:
self.armature = get_active_armature(context)
if not self.armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 5, "MMD Standardization") as progress:
# Step 1: Process bone names
self.process_bone_names(context)
progress.step("Processed bone names")
# Step 2: Fix bone structure
self.fix_bone_structure(context)
progress.step("Fixed bone structure")
# Step 3: Process weights
self.process_weights(context)
progress.step("Processed weights")
# Step 4: Clean up
self.cleanup_armature(context)
progress.step("Cleaned up armature")
# Step 5: Final validation
self.validate_results(context)
progress.step("Validated results")
self.report({'INFO'}, t("MMD.standardization_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"MMD Standardization failed: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def process_bone_names(self, context: Context) -> None:
"""Process and standardize bone names"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
# First pass - handle IK bones
ik_bones = [bone for bone in edit_bones if 'IK' in bone.name or 'IK' in bone.name]
for bone in ik_bones:
new_name = f"ik_{self.standardize_bone_name(bone.name.replace('IK', '').replace('IK', ''))}"
self.bone_mapping[bone.name] = new_name
bone.name = new_name
# Second pass - standard bones
for bone in edit_bones:
if bone not in ik_bones:
new_name = self.standardize_bone_name(bone.name)
if new_name != bone.name:
self.bone_mapping[bone.name] = new_name
bone.name = new_name
def translate_japanese_bone_name(self, name: str) -> str:
"""Translate Japanese bone names to English standardized names"""
name_lower = name.lower()
for bone_category, variations in bone_names.items():
for variation in variations:
if variation in name_lower:
return bone_category
return name
def standardize_bone_name(self, name: str) -> str:
"""Standardize individual bone names"""
result = self.translate_japanese_bone_name(name)
prefixes = ['ValveBiped_', 'Bip01_', 'MMD_', 'Armature|']
for prefix in prefixes:
if result.lower().startswith(prefix.lower()):
result = result[len(prefix):]
if result.endswith('_L') or result.endswith('.L'):
result = f"{result[:-2]}.L"
elif result.endswith('_R') or result.endswith('.R'):
result = f"{result[:-2]}.R"
return result
return result
def fix_bone_structure(self, context: Context) -> None:
"""Fix bone hierarchy and orientations"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
self.process_spine_chain(context)
self.fix_bone_orientations(context)
self.connect_bones(context)
def process_weights(self, context: Context) -> None:
"""Process and clean up vertex weights"""
for mesh in self.get_associated_meshes(context):
# Transfer weights based on bone mapping
for old_name, new_name in self.bone_mapping.items():
if old_name != new_name:
transfer_vertex_weights(mesh, old_name, new_name)
# Clean up zero weights
self.cleanup_vertex_groups(mesh, context)
def cleanup_armature(self, context: Context) -> None:
"""Perform final cleanup operations"""
self.remove_unused_bones(context)
self.cleanup_constraints(context)
self.fix_zero_length_bones(context)
def get_associated_meshes(self, context: Context) -> List[Object]:
"""Get all mesh objects associated with the armature"""
return [obj for obj in bpy.data.objects
if obj.type == 'MESH'
and obj.parent == self.armature]
def process_spine_chain(self, context: Context) -> None:
"""Process and fix spine bone chain hierarchy"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
spine_bones = {
'hips': None,
'spine': None,
'chest': None,
'upper_chest': None,
'neck': None,
'head': None
}
# Find spine bones using bone_names dictionary
for bone in edit_bones:
for spine_part, _ in spine_bones.items():
if any(alt_name in bone.name.lower() for alt_name in bone_names[spine_part]):
spine_bones[spine_part] = bone
break
# Set up spine hierarchy
hierarchy = [
('hips', 'spine'),
('spine', 'chest'),
('chest', 'neck'),
('neck', 'head')
]
for parent_name, child_name in hierarchy:
parent = spine_bones.get(parent_name)
child = spine_bones.get(child_name)
if parent and child:
child.parent = parent
child.use_connect = True
def fix_bone_orientations(self, context: Context) -> None:
"""Fix bone orientations for standard pose compatibility"""
edit_bones = self.armature.data.edit_bones
# Define standardized roll values for key bones
roll_values = {
'upper_arm.L': -0.1,
'upper_arm.R': 0.1,
'forearm.L': -0.1,
'forearm.R': 0.1,
'thigh.L': 0.0,
'thigh.R': 0.0,
'shin.L': 0.0,
'shin.R': 0.0,
'foot.L': 0.0,
'foot.R': 0.0,
'spine': 0.0,
'chest': 0.0,
'neck': 0.0
}
# Apply roll corrections
for bone in edit_bones:
if bone.name.lower() in roll_values:
bone.roll = roll_values[bone.name.lower()]
# Process arm chains
arm_pairs = [
('upper_arm', 'forearm'),
('forearm', 'hand')
]
for side in ['.L', '.R']:
for parent, child in arm_pairs:
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
# Process leg chains
leg_pairs = [
('thigh', 'shin'),
('shin', 'foot')
]
for side in ['.L', '.R']:
for parent, child in leg_pairs:
parent_bone = next((b for b in edit_bones if b.name.lower().startswith(parent) and b.name.endswith(side)), None)
child_bone = next((b for b in edit_bones if b.name.lower().startswith(child) and b.name.endswith(side)), None)
if parent_bone and child_bone:
child_bone.use_connect = True
child_bone.use_inherit_rotation = True
# Align twist bones if present
twist_bones = [b for b in edit_bones if 'twist' in b.name.lower()]
for twist_bone in twist_bones:
if twist_bone.parent:
twist_bone.roll = twist_bone.parent.roll
def remove_unused_bones(self, context: Context) -> None:
"""Remove unused and unnecessary bones from the armature"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
# Get list of bones that have vertex weights
used_bones = set()
for mesh in self.get_associated_meshes(context):
for group in mesh.vertex_groups:
used_bones.add(group.name)
# Get list of essential bones to always keep
essential_bones = {
'hips', 'spine', 'chest', 'upper_chest', 'neck', 'head',
'left_leg', 'right_leg', 'left_knee', 'right_knee',
'left_ankle', 'right_ankle', 'left_toe', 'right_toe'
}
# Add any additional bones you want to preserve
essential_bones.update(dont_delete_these_main_bones)
# Remove unused bones
for bone in edit_bones:
# Skip if bone is essential
if bone.name.lower() in essential_bones:
continue
# Skip if bone has weights
if bone.name in used_bones:
continue
# Remove the bone
edit_bones.remove(bone)
def connect_bones(self, context: Context) -> None:
"""Connect bones that should be connected in the hierarchy"""
edit_bones = self.armature.data.edit_bones
connect_chains = [
['hips', 'spine', 'chest', 'neck', 'head'],
['shoulder.L', 'upper_arm.L', 'forearm.L', 'hand.L'],
['shoulder.R', 'upper_arm.R', 'forearm.R', 'hand.R'],
['thigh.L', 'shin.L', 'foot.L', 'toe.L'],
['thigh.R', 'shin.R', 'foot.R', 'toe.R']
]
for chain in connect_chains:
prev_bone = None
for bone_name in chain:
bone = next((b for b in edit_bones if b.name.lower().endswith(bone_name.lower())), None)
if bone and prev_bone:
bone.parent = prev_bone
bone.use_connect = True
prev_bone = bone
def cleanup_vertex_groups(self, mesh_obj: Object, context: Context) -> None:
"""Clean up vertex groups by removing zero weights and merging similar groups"""
threshold = context.scene.avatar_toolkit.merge_weights_threshold
vertex_groups = mesh_obj.vertex_groups
groups_to_remove = set()
for group in vertex_groups:
weights = get_vertex_weights(mesh_obj, group.name)
if not any(weight > threshold for weight in weights.values()):
groups_to_remove.add(group.name)
for group_name in groups_to_remove:
group = vertex_groups.get(group_name)
if group:
vertex_groups.remove(group)
def validate_results(self, context: Context) -> None:
"""Validate the results of standardization"""
valid, messages = validate_armature(self.armature)
if not valid:
raise ValueError("\n".join(messages))
def cleanup_constraints(self, context: Context) -> None:
"""Remove all constraints from the armature."""
bpy.ops.object.mode_set(mode='POSE')
for pose_bone in self.armature.pose.bones:
constraints_to_remove = [constraint for constraint in pose_bone.constraints]
for constraint in constraints_to_remove:
pose_bone.constraints.remove(constraint)
def fix_zero_length_bones(self, context: Context) -> None:
"""Fix zero-length bones by setting minimal length"""
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = self.armature.data.edit_bones
min_length = 0.01 # Minimum bone length in Blender units
for bone in edit_bones:
bone_length = (bone.tail - bone.head).length
if bone_length < min_length:
if bone.parent:
direction = bone.parent.tail - bone.parent.head
direction.normalize()
else:
direction = Vector((0, 0, 1))
bone.tail = bone.head + (direction * min_length)
class ReparentMeshesOperator(bpy.types.Operator):
bl_idname = "avatar_toolkit.reparent_meshes"
bl_label = t("MMD.reparent_meshes")
bl_description = t("MMD.reparent_meshes_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and get_all_meshes(context)
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
meshes = get_all_meshes(context)
if not meshes:
self.report({'ERROR'}, t("MMD.no_meshes"))
return {'CANCELLED'}
try:
with ProgressTracker(context, len(meshes) + 1, "Reparenting Meshes") as progress:
# Get or create main collection
main_collection = self._get_main_collection(context)
progress.step("Setting up collections")
# Process each mesh
for mesh in meshes:
progress.step(f"Processing {mesh.name}")
self._process_mesh(mesh, armature, main_collection)
self.report({'INFO'}, t("MMD.reparenting_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error reparenting meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def _get_main_collection(self, context) -> bpy.types.Collection:
"""Get or create the main collection for the armature"""
if hasattr(context.scene, 'collection'):
return context.scene.collection
return context.scene.collection
def _process_mesh(self, mesh: bpy.types.Object,
armature: bpy.types.Object,
main_collection: bpy.types.Collection) -> None:
"""Process individual mesh parenting and collection management"""
# Unlink from other collections
for col in mesh.users_collection:
if col != main_collection:
col.objects.unlink(mesh)
# Ensure mesh is in main collection
if mesh.name not in main_collection.objects:
main_collection.objects.link(mesh)
# Set parent to armature
mesh.parent = armature
if not mesh.parent_type == 'ARMATURE':
mesh.parent_type = 'ARMATURE'
class AVATAR_TOOLKIT_OT_ConvertMmdMorphs(Operator):
"""Convert MMD morph data to shape keys"""
bl_idname = "avatar_toolkit.convert_mmd_morphs"
bl_label = t("MMD.convert_morphs")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and get_all_meshes(context)
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 3, "Converting MMD Morphs") as progress:
# Convert bone morphs to shape keys
if hasattr(armature, 'mmd_root') and armature.mmd_root.bone_morphs:
self.process_bone_morphs(context, armature, progress)
progress.step("Processed bone morphs")
# Clean up unused data
self.cleanup_unused_data(context)
progress.step("Cleaned up data")
# Validate results
self.validate_results(context)
progress.step("Validated results")
self.report({'INFO'}, t("MMD.conversion_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error converting MMD morphs: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def process_bone_morphs(self, context, armature, progress):
"""Process bone morphs into shape keys"""
for morph in armature.mmd_root.bone_morphs:
for mesh in get_all_meshes(context):
# Create armature modifier
mod = mesh.modifiers.new(morph.name, 'ARMATURE')
mod.object = armature
# Apply as shape key
with context.temp_override(object=mesh):
bpy.ops.object.modifier_apply(modifier=mod.name)
class AVATAR_TOOLKIT_OT_CleanupMmdModel(Operator):
"""Clean up MMD model by removing unused data and fixing display settings"""
bl_idname = "avatar_toolkit.cleanup_mmd"
bl_label = t("MMD.cleanup")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("MMD.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 4, "Cleaning MMD Model") as progress:
# Remove rigid bodies and joints
self.remove_physics_objects(armature)
progress.step("Removed physics objects")
# Clean up collections and hierarchy
self.cleanup_hierarchy(context, armature)
progress.step("Cleaned hierarchy")
# Fix viewport settings
self.fix_viewport_settings(context)
progress.step("Fixed viewport")
# Final cleanup
clear_unused_data_blocks()
progress.step("Cleared unused data")
self.report({'INFO'}, t("MMD.cleanup_complete"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error cleaning MMD model: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def remove_physics_objects(self, armature):
"""Remove physics-related objects"""
to_delete = []
for child in armature.children:
if any(x in child.name.lower() for x in ['rigidbodies', 'joints', 'physics']):
to_delete.append(child)
for obj in to_delete:
bpy.data.objects.remove(obj, do_unlink=True)
def cleanup_hierarchy(self, context, armature):
"""Clean up object hierarchy and collections"""
meshes = get_all_meshes(context)
for mesh in meshes:
# Ensure proper parenting
mesh.parent = armature
mesh.parent_type = 'ARMATURE'
# Clean up collections
for col in mesh.users_collection:
if col != context.scene.collection:
col.objects.unlink(mesh)
if mesh.name not in context.scene.collection.objects:
context.scene.collection.objects.link(mesh)
def fix_viewport_settings(self, context):
"""Fix viewport display settings"""
# Set armature display
armature = get_active_armature(context)
armature.data.display_type = 'OCTAHEDRAL'
armature.show_in_front = True
# Set viewport shading
for area in context.screen.areas:
if area.type == 'VIEW_3D':
space = area.spaces[0]
space.shading.type = 'MATERIAL'
space.clip_start = 0.01
space.clip_end = 300
class AVATAR_TOOLKIT_OT_FixMeshes(Operator):
"""Clean up and optimize mesh materials, shading, and shape keys"""
bl_idname = "avatar_toolkit.fix_meshes"
bl_label = t("Optimization.fix_meshes")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = get_active_armature(context)
return armature is not None and get_all_meshes(context)
def execute(self, context):
try:
meshes = get_all_meshes(context)
if not meshes:
self.report({'ERROR'}, t("Optimization.no_meshes"))
return {'CANCELLED'}
with ProgressTracker(context, len(meshes), "Fixing Meshes") as progress:
for mesh in meshes:
self.process_mesh(context, mesh)
progress.step(f"Processed {mesh.name}")
self.report({'INFO'}, t("Optimization.meshes_fixed"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error fixing meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def process_mesh(self, context: Context, mesh: Object) -> None:
"""Process and fix individual mesh"""
# Unlock transforms
for i in range(3):
mesh.lock_location[i] = False
mesh.lock_rotation[i] = False
mesh.lock_scale[i] = False
# Process shape keys
if mesh.data.shape_keys:
self.fix_shape_keys(mesh)
# Process materials
self.fix_materials(context, mesh)
def fix_shape_keys(self, mesh: Object) -> None:
"""Fix and clean up shape keys"""
if not mesh.data.shape_keys:
return
shape_keys = mesh.data.shape_keys.key_blocks
# Rename basis
if shape_keys[0].name != "Basis":
shape_keys[0].name = "Basis"
# Clean up names
for key in shape_keys:
# Remove common prefixes/suffixes
clean_name = key.name
for prefix in ['Face.M F00 000 Fcl ', 'Face.M F00 000 00 Fcl ']:
clean_name = clean_name.replace(prefix, '')
# Replace underscores with spaces
clean_name = clean_name.replace('_', ' ')
key.name = clean_name
# Sort shape keys by category
categories = ['MTH', 'EYE', 'BRW', 'ALL']
# Create sorted list of shape key names
ordered_names = []
# Add categorized keys first
for category in categories:
category_keys = [key.name for key in shape_keys if key.name.startswith(category)]
ordered_names.extend(sorted(category_keys))
# Add remaining keys
remaining = [key.name for key in shape_keys if not any(key.name.startswith(c) for c in categories)]
ordered_names.extend(sorted(remaining))
# Reorder using context override
with bpy.context.temp_override(active_object=mesh, selected_objects=[mesh]):
for idx, name in enumerate(ordered_names):
mesh.active_shape_key_index = shape_keys.find(name)
while mesh.active_shape_key_index > idx:
bpy.ops.object.shape_key_move(type='UP')
def fix_materials(self, context: Context, mesh: Object) -> None:
"""Fix and optimize materials"""
for slot in mesh.material_slots:
if not slot.material:
continue
material = slot.material
# Set up basic material properties
material.use_backface_culling = True
material.blend_method = 'HASHED'
material.shadow_method = 'HASHED'
# Clean up material name
material.name = self.clean_material_name(material.name)
# Consolidate similar materials
for other_slot in mesh.material_slots:
if other_slot.material and other_slot.material != material:
if materials_match(material, other_slot.material):
other_slot.material = material
def clean_material_name(self, name: str) -> str:
"""Clean up material name"""
# Remove common prefixes/suffixes
prefixes = ['material', 'mat', 'mtl', 'material.']
for prefix in prefixes:
if name.lower().startswith(prefix):
name = name[len(prefix):]
# Remove numbers at end
while name and name[-1].isdigit():
name = name[:-1]
return name.strip()
class AVATAR_TOOLKIT_OT_ValidateMeshes(Operator):
"""Validate meshes and UV maps for common issues"""
bl_idname = "avatar_toolkit.validate_meshes"
bl_label = t("Validation.check_meshes")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
armature = get_active_armature(context)
if not armature:
self.report({'ERROR'}, t("Validation.no_armature"))
return {'CANCELLED'}
try:
with ProgressTracker(context, 3, "Validating Meshes") as progress:
# Check bone hierarchy
hierarchy_issues = self.validate_bone_hierarchy(armature)
progress.step("Checked bone hierarchy")
# Check UV coordinates
uv_issues = self.validate_uv_maps(context)
progress.step("Checked UV maps")
# Generate report
self.generate_validation_report(context, hierarchy_issues, uv_issues)
progress.step("Generated report")
return {'FINISHED'}
except Exception as e:
logger.error(f"Error validating meshes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def validate_bone_hierarchy(self, armature: Object) -> List[str]:
"""Validate bone hierarchy against standard structure"""
issues = []
# Define expected hierarchy
hierarchy = [
['hips', 'spine', 'chest', 'neck', 'head'],
['hips', 'left_leg', 'left_knee', 'left_ankle'],
['hips', 'right_leg', 'right_knee', 'right_ankle'],
['chest', 'left_shoulder', 'left_arm', 'left_elbow', 'left_wrist'],
['chest', 'right_shoulder', 'right_arm', 'right_elbow', 'right_wrist']
]
for chain in hierarchy:
previous = None
for bone_name in chain:
# Check if bone exists
bone = None
for alt_name in bone_names[bone_name]:
if alt_name in armature.data.bones:
bone = armature.data.bones[alt_name]
break
if not bone:
issues.append(t("Validation.missing_bone", bone=bone_name))
continue
# Check parent relationship
if previous:
if not bone.parent:
issues.append(t("Validation.no_parent", bone=bone.name))
elif bone.parent.name != previous.name:
issues.append(t("Validation.wrong_parent",
bone=bone.name,
expected=previous.name,
actual=bone.parent.name))
previous = bone
return issues
def validate_uv_maps(self, context: Context) -> Dict[str, int]:
"""Check UV maps for issues"""
issues = {'nan_coords': 0, 'missing_uvs': 0}
for mesh in get_all_meshes(context):
if not mesh.data.uv_layers:
issues['missing_uvs'] += 1
continue
for uv_layer in mesh.data.uv_layers:
for uv in uv_layer.data:
if math.isnan(uv.uv.x):
uv.uv.x = 0
issues['nan_coords'] += 1
if math.isnan(uv.uv.y):
uv.uv.y = 0
issues['nan_coords'] += 1
return issues
def generate_validation_report(self, context: Context,
hierarchy_issues: List[str],
uv_issues: Dict[str, int]) -> None:
"""Generate and display validation report"""
report_lines = []
# Add hierarchy issues
if hierarchy_issues:
report_lines.append(t("Validation.hierarchy_issues"))
report_lines.extend(hierarchy_issues)
# Add UV issues
if uv_issues['nan_coords'] > 0:
report_lines.append(t("Validation.uv_nan_coords",
count=uv_issues['nan_coords']))
if uv_issues['missing_uvs'] > 0:
report_lines.append(t("Validation.missing_uvs",
count=uv_issues['missing_uvs']))
# Show report
if report_lines:
self.report({'WARNING'}, "\n".join(report_lines))
else:
self.report({'INFO'}, t("Validation.no_issues"))
View File
+175
View File
@@ -0,0 +1,175 @@
import bpy
import re
from typing import Set, Dict, List, Optional, Tuple
from bpy.types import (
Operator,
Context,
Object,
Material,
NodeTree,
ShaderNodeTexImage
)
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
clear_unused_data_blocks,
ProgressTracker
)
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
"""Compare two texture nodes for matching properties and image data"""
return tex1.image == tex2.image and tex1.extension == tex2.extension
def consolidate_nodes(node1: ShaderNodeTexImage, node2: ShaderNodeTexImage) -> None:
"""Transfer properties from one texture node to another to ensure consistency"""
node2.color_space = node1.color_space
node2.coordinates = node1.coordinates
def consolidate_textures(node_tree1: NodeTree, node_tree2: NodeTree) -> None:
"""Synchronize texture nodes between two material node trees"""
for node1 in node_tree1.nodes:
if node1.type == 'TEX_IMAGE':
for node2 in node_tree2.nodes:
if (node2.type == 'TEX_IMAGE' and node1.image == node2.image):
consolidate_nodes(node1, node2)
node2.image = node1.image
elif node1.type == 'GROUP':
if node1.node_tree and node2.node_tree:
consolidate_textures(node1.node_tree, node2.node_tree)
def color_match(col1: Tuple[float, ...], col2: Tuple[float, ...], tolerance: float = 0.01) -> bool:
"""Compare two color values within a specified tolerance"""
return all(abs(c1 - c2) < tolerance for c1, c2 in zip(col1, col2))
def materials_match(mat1: Material, mat2: Material, tolerance: float = 0.01) -> bool:
"""Compare two materials for matching properties within tolerance"""
if not color_match(mat1.diffuse_color, mat2.diffuse_color, tolerance):
return False
if abs(mat1.roughness - mat2.roughness) > tolerance:
return False
if abs(mat1.metallic - mat2.metallic) > tolerance:
return False
if abs(mat1.alpha_threshold - mat2.alpha_threshold) > tolerance:
return False
if not color_match(mat1.emission_color, mat2.emission_color, tolerance):
return False
if mat1.node_tree and mat2.node_tree:
consolidate_textures(mat1.node_tree, mat2.node_tree)
return True
def get_base_name(name: str) -> str:
"""Extract the base material name by removing numeric suffixes"""
mat_match = re.match(r"^(.*)\.\d{3}$", name)
return mat_match.group(1) if mat_match else name
class AvatarToolkit_OT_CombineMaterials(Operator):
"""Operator for combining similar materials to reduce duplicate materials"""
bl_idname: str = "avatar_toolkit.combine_materials"
bl_label: str = t("Optimization.combine_materials")
bl_description: str = t("Optimization.combine_materials_desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if the operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
"""Execute the material combination operation"""
try:
armature = get_active_armature(context)
meshes = get_all_meshes(context)
if not meshes:
self.report({'WARNING'}, t("Optimization.no_meshes"))
return {'CANCELLED'}
if not any(mesh.material_slots for mesh in meshes):
self.report({'WARNING'}, t("Optimization.no_materials"))
return {'CANCELLED'}
with ProgressTracker(context, 4, "Combining Materials") as progress:
try:
num_combined = self.consolidate_materials(meshes)
except Exception as e:
logger.error(f"Material consolidation failed: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.consolidation"))
return {'CANCELLED'}
progress.step("Consolidated materials")
try:
num_cleaned = self.clean_material_slots(meshes)
except Exception as e:
logger.error(f"Material slot cleanup failed: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.slot_cleanup"))
return {'CANCELLED'}
progress.step("Cleaned material slots")
try:
num_removed = clear_unused_data_blocks()
except Exception as e:
logger.error(f"Data block cleanup failed: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.data_cleanup"))
return {'CANCELLED'}
progress.step("Removed unused data blocks")
self.report({'INFO'}, t("Optimization.materials_combined",
combined=num_combined,
cleaned=num_cleaned,
removed=num_removed))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to combine materials: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.combine_materials", error=str(e)))
return {'CANCELLED'}
def consolidate_materials(self, meshes: List[Object]) -> int:
"""Consolidate similar materials across all meshes"""
mat_mapping: Dict[str, Material] = {}
num_combined: int = 0
for mesh in meshes:
for slot in mesh.material_slots:
mat: Optional[Material] = slot.material
if mat:
base_name: str = get_base_name(mat.name)
if base_name in mat_mapping:
base_mat: Material = mat_mapping[base_name]
try:
if materials_match(base_mat, mat):
consolidate_textures(base_mat.node_tree, mat.node_tree)
num_combined += 1
slot.material = base_mat
except AttributeError:
logger.warning(f"Material attribute mismatch: {mat.name}")
continue
else:
mat_mapping[base_name] = mat
return num_combined
def clean_material_slots(self, meshes: List[Object]) -> int:
"""Remove unused material slots from meshes"""
cleaned_slots = 0
for obj in meshes:
initial_slots = len(obj.material_slots)
bpy.context.view_layer.objects.active = obj
bpy.ops.object.material_slot_remove_unused()
cleaned_slots += initial_slots - len(obj.material_slots)
return cleaned_slots
+101
View File
@@ -0,0 +1,101 @@
import bpy
from typing import Set, List, Tuple, ClassVar
from bpy.types import Operator, Context, Object
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
validate_meshes,
join_mesh_objects,
ProgressTracker
)
class AvatarToolkit_OT_JoinAllMeshes(Operator):
"""Operator to join all meshes in the scene"""
bl_idname: ClassVar[str] = "avatar_toolkit.join_all_meshes"
bl_label: ClassVar[str] = t("Optimization.join_all_meshes")
bl_description: ClassVar[str] = t("Optimization.join_all_meshes_desc")
bl_options: ClassVar[Set[str]] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature: Object | None = get_active_armature(context)
if not armature:
return False
valid: bool
valid, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
try:
armature: Object = get_active_armature(context)
meshes: List[Object] = get_all_meshes(context)
valid: bool
message: str
valid, message = validate_meshes(meshes)
if not valid:
self.report({'WARNING'}, message)
return {'CANCELLED'}
with ProgressTracker(context, 5, "Joining All Meshes") as progress:
joined_mesh = join_mesh_objects(context, meshes, progress)
if joined_mesh:
context.view_layer.objects.active = armature
self.report({'INFO'}, t("Optimization.meshes_joined"))
return {'FINISHED'}
else:
self.report({'ERROR'}, t("Optimization.error.join_meshes"))
return {'CANCELLED'}
except Exception as e:
logger.error(f"Failed to join meshes: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.join_meshes", error=str(e)))
return {'CANCELLED'}
class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
"""Operator to join selected meshes"""
bl_idname: ClassVar[str] = "avatar_toolkit.join_selected_meshes"
bl_label: ClassVar[str] = t("Optimization.join_selected_meshes")
bl_description: ClassVar[str] = t("Optimization.join_selected_meshes_desc")
bl_options: ClassVar[Set[str]] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature: Object | None = get_active_armature(context)
if not armature:
return False
valid: bool
valid, _ = validate_armature(armature)
return (valid and
context.mode == 'OBJECT' and
len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1)
def execute(self, context: Context) -> Set[str]:
try:
selected_meshes: List[Object] = [obj for obj in context.selected_objects if obj.type == 'MESH']
valid: bool
message: str
valid, message = validate_meshes(selected_meshes)
if not valid:
self.report({'WARNING'}, message)
return {'CANCELLED'}
with ProgressTracker(context, 5, "Joining Selected Meshes") as progress:
joined_mesh = join_mesh_objects(context, selected_meshes, progress)
if joined_mesh:
self.report({'INFO'}, t("Optimization.selected_meshes_joined"))
return {'FINISHED'}
else:
self.report({'ERROR'}, t("Optimization.error.join_selected"))
return {'CANCELLED'}
except Exception as e:
logger.error(f"Failed to join selected meshes: {str(e)}")
self.report({'ERROR'}, t("Optimization.error.join_selected", error=str(e)))
return {'CANCELLED'}
+281
View File
@@ -0,0 +1,281 @@
import bpy
import numpy as np
from typing import List, TypedDict, Any, Literal, TypeAlias, cast
from bpy.types import Operator, Context, Object, Event
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature
)
# Constants
MERGE_ITERATION_COUNT = 20
MERGE_DISTANCE_DEFAULT = 0.0001
# Type definitions
ModalReturnType: TypeAlias = Literal['RUNNING_MODAL', 'FINISHED', 'CANCELLED']
class MeshEntry(TypedDict):
mesh: Object
shapekeys: list[str]
vertices: int
cur_vertex_pass: int
def create_duplicate_for_merge(context: Context, mesh: Object, shapekey_name: str) -> Object:
"""Creates a duplicate mesh object for merge testing"""
context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
bpy.ops.object.duplicate()
bpy.ops.object.shape_key_move(type='TOP')
duplicate = context.view_layer.objects.active
duplicate.name = f"{shapekey_name}_object_is_{mesh.name}"
return duplicate
def process_vertex_merging(mesh_data: bpy.types.Mesh, vertices_original: dict[int, Any], current_vertex: int) -> list[int]:
"""Process vertex merging and return merged vertex indices"""
merged_vertices = []
i, j = 0, 0
while i < len(vertices_original):
if j + 1 > len(mesh_data.vertices):
merged_vertices.append(i)
j = j - 1
elif mesh_data.vertices[j].co.xyz != vertices_original[i]:
merged_vertices.append(i)
j = j - 1
elif vertices_original[i] == vertices_original[current_vertex]:
merged_vertices.append(i)
i, j = i + 1, j + 1
return merged_vertices
class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
bl_idname = "avatar_toolkit.remove_doubles_advanced"
bl_label = t("Optimization.remove_doubles_advanced")
bl_description = t("Optimization.remove_doubles_advanced_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if the operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> set[str]:
"""Execute the advanced remove doubles operator"""
context.scene.avatar_toolkit.remove_doubles_advanced = True
bpy.ops.avatar_toolkit.remove_doubles('INVOKE_DEFAULT')
return {'RUNNING_MODAL'}
class AvatarToolkit_OT_RemoveDoubles(Operator):
bl_idname = "avatar_toolkit.remove_doubles"
bl_label = t("Optimization.remove_doubles")
bl_description = t("Optimization.remove_doubles_desc")
bl_options = {'REGISTER', 'UNDO'}
objects_to_do: list[MeshEntry] = []
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if the operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid
def draw(self, context: Context) -> None:
"""Draw the operator's UI"""
layout = self.layout
layout.prop(context.scene.avatar_toolkit, "remove_doubles_merge_distance")
layout.label(text=t("Optimization.remove_doubles_warning"))
layout.label(text=t("Optimization.remove_doubles_wait"))
def invoke(self, context: Context, event: Event) -> set[str]:
"""Initialize the operator"""
logger.info("Starting modal execution of merge doubles safely")
return context.window_manager.invoke_props_dialog(self)
def setup_mesh_entry(self, mesh: Object) -> MeshEntry:
"""Set up mesh entry data structure"""
mesh_entry: MeshEntry = {
"mesh": mesh,
"shapekeys": [],
"vertices": len(mesh.data.vertices),
"cur_vertex_pass": 0
}
if mesh.data.shape_keys:
mesh_entry["shapekeys"] = [shape.name for shape in mesh.data.shape_keys.key_blocks]
return mesh_entry
def execute(self, context: Context) -> set[str]:
"""Execute the remove doubles operator"""
try:
armature = get_active_armature(context)
if not armature:
self.report({'WARNING'}, t("Optimization.no_armature"))
return {'CANCELLED'}
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
objects = get_all_meshes(context)
self.objects_to_do = []
for mesh in objects:
if mesh.data.name not in [obj["mesh"].data.name for obj in self.objects_to_do]:
logger.debug(f"Setting up data for object {mesh.name}")
mesh_entry = self.setup_mesh_entry(mesh)
self.objects_to_do.append(mesh_entry)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
except Exception as e:
logger.error(f"Error in execute: {str(e)}")
return {'CANCELLED'}
def modify_mesh(self, context: Context, mesh: MeshEntry) -> None:
"""Basic mesh modification for simple cases"""
try:
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
mesh_data = mesh["mesh"].data
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
# Select vertices with different positions in shape keys
for index, point in enumerate(mesh["mesh"].active_shape_key.points):
if point.co.xyz != mesh_data.shape_keys.key_blocks[0].points[index].co.xyz:
mesh_data.vertices[index].select = True
logger.debug(f"Shapekey has moved vertex at index {index}")
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
except Exception as e:
logger.error(f"Error in modify_mesh: {str(e)}")
def modify_mesh_advanced(self, context: Context, mesh_entry: MeshEntry) -> bool:
"""Advanced mesh modification with shape key handling"""
try:
final_merged_vertex_group = []
initialized_final = False
merge_distance = context.scene.avatar_toolkit.remove_doubles_merge_distance
for shapekey_name in mesh_entry["shapekeys"]:
duplicate = create_duplicate_for_merge(context, mesh_entry["mesh"], shapekey_name)
vertices_original = {i: v.co.xyz for i, v in enumerate(duplicate.data.vertices)}
# Process merging
merged_vertices = process_vertex_merging(duplicate.data, vertices_original, mesh_entry["cur_vertex_pass"])
if not initialized_final:
final_merged_vertex_group = merged_vertices.copy()
initialized_final = True
else:
final_merged_vertex_group = [v for v in final_merged_vertex_group if v in merged_vertices]
bpy.ops.object.delete()
# Apply final merging
if final_merged_vertex_group:
self.apply_final_merging(context, mesh_entry, final_merged_vertex_group, merge_distance)
return not (len(final_merged_vertex_group) > 1)
except Exception as e:
logger.error(f"Error in modify_mesh_advanced: {str(e)}")
return True
def apply_final_merging(self, context: Context, mesh_entry: MeshEntry, vertex_group: list[int], merge_distance: float) -> None:
"""Apply final vertex merging operations"""
mesh = mesh_entry["mesh"]
context.view_layer.objects.active = mesh
mesh.select_set(True)
bpy.ops.object.mode_set(mode='OBJECT')
select_target_group = [False] * len(mesh.data.vertices)
for vertex_index in vertex_group:
select_target_group[vertex_index] = True
mesh.data.vertices.foreach_set("select", select_target_group)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
def process_simple_mesh(self, context: Context, mesh: MeshEntry, merge_distance: float) -> None:
"""Process mesh without shapekeys using simple merge operation"""
logger.debug(f"Processing mesh without shapekeys: {mesh['mesh'].name}")
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
mesh["mesh"].data.vertices.foreach_set("select", [False] * len(mesh["mesh"].data.vertices))
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
def finish_mesh_processing(self, context: Context, mesh: MeshEntry, advanced: bool, merge_distance: float) -> None:
"""Complete the mesh processing by performing final merge operations"""
logger.debug("Finishing mesh processing")
if not advanced:
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=merge_distance, use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
def modal(self, context: Context, event: Event) -> set[ModalReturnType]:
"""Modal operator execution"""
try:
if not self.objects_to_do:
self.report({'INFO'}, t("Optimization.remove_doubles_completed"))
logger.info("Finishing modal execution of merge doubles safely")
return {'FINISHED'}
mesh = self.objects_to_do[0]
mesh_data = mesh["mesh"].data
advanced = context.scene.avatar_toolkit.remove_doubles_advanced
merge_distance = context.scene.avatar_toolkit.remove_doubles_merge_distance
if len(mesh['shapekeys']) > 0 and not advanced:
shapekeyname = mesh['shapekeys'].pop(0)
mesh["mesh"].active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekeyname)
logger.debug(f"Processing shapekey {shapekeyname}")
self.modify_mesh(context, mesh)
elif not mesh_data.shape_keys:
self.process_simple_mesh(context, mesh, merge_distance)
self.objects_to_do.pop(0)
elif not (mesh["cur_vertex_pass"] > mesh["vertices"]) and advanced:
if self.modify_mesh_advanced(context, mesh):
mesh["cur_vertex_pass"] += 1
else:
self.finish_mesh_processing(context, mesh, advanced, merge_distance)
self.objects_to_do.pop(0)
return {'RUNNING_MODAL'}
except Exception as e:
logger.error(f"Error in modal: {str(e)}")
return {'CANCELLED'}
+166
View File
@@ -0,0 +1,166 @@
import bpy
from typing import Set, Dict, List, Tuple, Optional, Any
from bpy.props import StringProperty
from bpy.types import Operator, Context, Object, Event, Modifier
from ..core.logging_setup import logger
from ..core.translations import t
from ..core.common import (
get_active_armature,
get_all_meshes,
apply_pose_as_rest,
validate_armature,
cache_vertex_positions,
apply_vertex_positions,
validate_mesh_for_pose,
process_armature_modifiers,
ProgressTracker
)
class BatchPoseOperationMixin:
"""Base class for batch pose operations"""
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid and context.mode == 'POSE'
def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]:
"""Validate meshes for pose operations"""
invalid_meshes = []
for mesh in meshes:
valid, message = validate_mesh_for_pose(mesh)
if not valid:
invalid_meshes.append((mesh, message))
return invalid_meshes
class AvatarToolkit_OT_StartPoseMode(Operator):
bl_idname = 'avatar_toolkit.start_pose_mode'
bl_label = t("QuickAccess.start_pose_mode.label")
bl_description = t("QuickAccess.start_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature or context.mode == "POSE":
return False
valid, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
logger.info(f"Starting pose mode for armature: {armature.name}")
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = armature
armature.select_set(True)
bpy.ops.object.mode_set(mode='POSE')
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to start pose mode: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.start", error=str(e)))
return {'CANCELLED'}
class AvatarToolkit_OT_StopPoseMode(Operator):
bl_idname = 'avatar_toolkit.stop_pose_mode'
bl_label = t("QuickAccess.stop_pose_mode.label")
bl_description = t("QuickAccess.stop_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return get_active_armature(context) and context.mode == "POSE"
def execute(self, context: Context) -> Set[str]:
try:
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to stop pose mode: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.stop", error=str(e)))
return {'CANCELLED'}
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
bl_options = {'REGISTER', 'UNDO'}
shapekey_name: StringProperty(
name=t("PoseMode.shapekey.name"),
description=t("PoseMode.shapekey.description"),
default=t("PoseMode.shapekey.default")
)
def invoke(self, context: Context, event: Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context) -> Set[str]:
try:
meshes = get_all_meshes(context)
invalid_meshes = self.validate_meshes(meshes)
if invalid_meshes:
message = "\n".join(f"{mesh.name}: {reason}" for mesh, reason in invalid_meshes)
self.report({'WARNING'}, t("PoseMode.skipped_meshes", message=message))
valid_meshes = [mesh for mesh in meshes if mesh not in [m for m, _ in invalid_meshes]]
with ProgressTracker(context, len(valid_meshes), "Applying Pose as Shape Key") as progress:
for mesh_obj in valid_meshes:
if not mesh_obj.data.shape_keys:
mesh_obj.shape_key_add(name=t("PoseMode.basis"))
new_shape = mesh_obj.shape_key_add(name=self.shapekey_name, from_mix=False)
cached_positions = cache_vertex_positions(
mesh_obj.evaluated_get(context.evaluated_depsgraph_get())
)
apply_vertex_positions(new_shape.data, cached_positions)
progress.step(f"Processed {mesh_obj.name}")
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply pose as shape key: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=str(e)))
return {'CANCELLED'}
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
bl_label = t("QuickAccess.apply_pose_as_rest.label")
bl_description = t("QuickAccess.apply_pose_as_rest.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> Set[str]:
try:
armature_obj = get_active_armature(context)
meshes = get_all_meshes(context)
invalid_meshes = self.validate_meshes(meshes)
if invalid_meshes:
message = "\n".join(f"{mesh.name}: {reason}" for mesh, reason in invalid_meshes)
self.report({'WARNING'}, t("PoseMode.skipped_meshes", message=message))
valid_meshes = [mesh for mesh in meshes if mesh not in [m for m, _ in invalid_meshes]]
with ProgressTracker(context, len(valid_meshes) + 2, "Applying Pose as Rest") as progress:
success, message = apply_pose_as_rest(context, armature_obj, valid_meshes)
if not success:
raise ValueError(message)
progress.step("Applied pose to armature")
logger.info("Successfully applied pose as rest")
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply pose as rest: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.rest_pose", error=str(e)))
return {'CANCELLED'}
-309
View File
@@ -1,309 +0,0 @@
import bpy
from typing import List, TypedDict, Any
from bpy.types import Operator, Context, Object
from ..core.register import register_wrap
from ..core.common import get_selected_armature, is_valid_armature, select_current_armature, get_all_meshes
from ..functions.translations import t
class meshEntry(TypedDict):
mesh: Object
shapekeys: list[str]
vertices: int
cur_vertex_pass: int
@register_wrap
class AvatarToolKit_OT_RemoveDoublesSafelyAdvanced(Operator):
bl_idname = "avatar_toolkit.remove_doubles_safely_advanced"
bl_label = t("Optimization.remove_doubles_safely_advanced.label")
bl_description = t("Optimization.remove_doubles_safely_advanced.desc")
bl_options = {'REGISTER', 'UNDO'}
merge_distance: bpy.props.FloatProperty(default=0.0001)
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def draw(self, context):
layout = self.layout
layout.label(text="This process may take a long time.")
layout.label(text="Blender may seem unresponsive during this operation.")
layout.label(text="Please be patient and wait for it to complete.")
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: Context):
bpy.ops.avatar_toolkit.remove_doubles_safely('INVOKE_DEFAULT', advanced=True, merge_distance=self.merge_distance)
return {'RUNNING_MODAL'}
@register_wrap
class AvatarToolKit_OT_RemoveDoublesSafely(Operator):
bl_idname = "avatar_toolkit.remove_doubles_safely"
bl_label = t("Optimization.remove_doubles_safely.label")
bl_description = t("Optimization.remove_doubles_safely.desc")
bl_options = {'REGISTER', 'UNDO'}
objects_to_do: list[meshEntry] = []
merge_distance: bpy.props.FloatProperty(default=0.0001)
advanced: bpy.props.BoolProperty(default=False)
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> set:
if not select_current_armature(context):
self.report({'WARNING'}, t("Optimization.no_armature_selected"))
return {'CANCELLED'}
armature = get_selected_armature(context)
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
objects: List[Object] = get_all_meshes(context)
self.objects_to_do = []
for mesh in objects:
if mesh.data.name not in [stored_object["mesh"].data.name for stored_object in self.objects_to_do]:
print("setting up data for object" + mesh.name)
mesh_shapekeys = {"mesh":mesh,"shapekeys":[],"vertices":0,"cur_vertex_pass":0}
mesh_data: bpy.types.Mesh = mesh.data
shape: bpy.types.ShapeKey = None
mesh_shapekeys["vertices"] = len(mesh_data.vertices)
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
if mesh_data.shape_keys:
for shape in mesh_data.shape_keys.key_blocks:
mesh_shapekeys["shapekeys"].append(shape.name)
if self.advanced:
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = mesh
bpy.ops.object.select_all(action='DESELECT')
print("queued data for "+mesh.name+" is: ")
print(mesh_shapekeys)
self.objects_to_do.append(mesh_shapekeys)
return {'FINISHED'}
def invoke(self, context: Context, event: bpy.types.Event) -> set:
print("==================")
print("==================")
print("==================")
print("==================")
print("starting modal execution of merge doubles safely.")
print("==================")
print("==================")
print("==================")
print("==================")
self.execute(context)
context.window_manager.modal_handler_add(self)
return {'RUNNING_MODAL'}
def modify_mesh(self, context: Context, mesh: meshEntry):
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
mesh_data: bpy.types.Mesh = mesh["mesh"].data
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
for index, point in enumerate(mesh["mesh"].active_shape_key.points):
if point.co.xyz != mesh_data.shape_keys.key_blocks[0].points[index].co.xyz:
mesh_data.vertices[index].select = True
print("shapekey has a moved vertex at index \""+str(index)+"\", excluding from simple double merging!")
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
print("finished shapekey basic.")
def modify_mesh_advanced(self, context: Context, mesh_entry: meshEntry):
final_merged_vertex_group: list[int] = []
initialized_final: bool = False
for shapekey_name in mesh_entry["shapekeys"]:
mesh = mesh_entry["mesh"]
#make a copy to do double merge testing on for the current vertex
context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = mesh
mesh_data: bpy.types.Mesh = mesh.data
vertices_original: dict[int,Any] = {}
original_count: int = len(mesh_data.vertices)
mesh.select_set(True)
mesh.active_shape_key_index = mesh_data.shape_keys.key_blocks.find(shapekey_name)
bpy.ops.object.duplicate()
bpy.ops.object.shape_key_move(type='TOP')
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.shape_key_remove(all=True, apply_mix=False)
mesh = context.view_layer.objects.active
mesh.name = shapekey_name+"_object_is_"+mesh.name
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
context.view_layer.objects.active = mesh
mesh_data: bpy.types.Mesh = mesh.data
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
for index, merged_point in enumerate(mesh_data.vertices):
vertices_original[index] = merged_point.co.xyz
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
select_target_vertex = [False]*len(mesh_data.vertices)
try:
select_target_vertex[mesh_entry["cur_vertex_pass"]] = True
except:
bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless.
return True
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",select_target_vertex)
bpy.ops.object.mode_set(mode='EDIT')
for i in range(0,20): #for some reason, if using merge to unselected on a vertex, the vertex will only merge to 1 other vertex. so we gotta spam it to fix it.
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=True, use_sharp_edge_from_normals=False)
bpy.ops.object.mode_set(mode='OBJECT')
merged_vertices: list[int] = []
mesh_data_vertices: dict[int,Any] = {}
for idx,vertex in enumerate(mesh_data.vertices):
mesh_data_vertices[idx] = vertex.co.xyz
#I'm loosing my mind with indices because I cannot keep so many numbers in my head. I will have to use 2 pointers
# yes this can be simplified more, but the mountains of errors with using a normal for statement are making me
# loose my mind. This is hard. - @989onan
#Below is the magic that determines whether or not vertices were merged and then puts the vertices
#that were merged into a list. - @989onan
i = 0
j = 0
while(i<len(vertices_original)):
if j+1 > len(mesh_data.vertices):
merged_vertices.append(i)
j = j-1
elif mesh_data.vertices[j].co.xyz != vertices_original[i]:
merged_vertices.append(i)
j = j-1
elif vertices_original[i] == vertices_original[mesh_entry["cur_vertex_pass"]]:
merged_vertices.append(i)
i = i+1
j = j+1
#give our final set of points some inital data. we're looking for points that are merged on every shape key (and therefore appear in every version of merged_vertices).
# If we initialize the array with points from the first version of merged_vertices, then we can remove the vertices from final that don't get merged from
#every future version of merged_vertices with the "if merged_point not in merged_vertices:" code.
if initialized_final == False:
for point in merged_vertices:
final_merged_vertex_group.append(point)
initialized_final = True
#iterate through a copy of final vertex groups to prevent crash. If a vertex was merged before, but didn't merge in this vertex,
# then the vertex shouldn't be merged because it moves away from the vertex we are double merging now (ex: bottom of mouth moving away from top when opening on a shapekey) - @989onan
for merged_point in final_merged_vertex_group[:]:
if merged_point not in merged_vertices:
final_merged_vertex_group.remove(merged_point)
bpy.ops.object.mode_set(mode='OBJECT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.delete() #remove our double merge testing object for this shapekey, since we merged doubles on it, it will be useless.
context.view_layer.objects.active = mesh_entry["mesh"]
bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = mesh_entry["mesh"]
mesh_entry["mesh"].select_set(True)
original_mesh_data: bpy.types.Mesh = mesh_entry["mesh"].data
select_target_group = [False]*len(original_mesh_data.vertices)
for vertex_index in final_merged_vertex_group:
select_target_group[vertex_index] = True
bpy.ops.object.mode_set(mode='OBJECT')
original_mesh_data.vertices.foreach_set("select",select_target_group)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance, use_unselected=False, use_sharp_edge_from_normals=False)
bpy.ops.object.mode_set(mode='OBJECT')
original_mesh_data.vertices.foreach_set("select",[False]*len(original_mesh_data.vertices))
print("finished advanced merge doubles for single vertex at index: "+str(mesh_entry["cur_vertex_pass"]))
return not (len(final_merged_vertex_group) > 1)
def modal(self, context: Context, event: bpy.types.Event) -> set:
if len(self.objects_to_do) > 0:
bpy.ops.object.select_all(action='DESELECT')
mesh: meshEntry = self.objects_to_do[0]
mesh_data: bpy.types.Mesh = mesh["mesh"].data
if (len(mesh['shapekeys']) > 0) and (not self.advanced):
shapekeyname: str = mesh['shapekeys'].pop(0)
target_shapekey: int = mesh_data.shape_keys.key_blocks.find(shapekeyname)
mesh["mesh"].active_shape_key_index = target_shapekey
print("doing shapekey \""+shapekeyname+"\" on mesh \""+mesh['mesh'].name+"\".")
self.modify_mesh(context, mesh)
elif not (mesh_data.shape_keys):
print("doing mesh with no shapekeys named \""+mesh['mesh'].name+"\".")
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
mesh_data.vertices.foreach_set("select",[False]*len(mesh_data.vertices))
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
self.objects_to_do.pop(0)
elif (not (mesh["cur_vertex_pass"] > mesh["vertices"])) and self.advanced:
print("doing a merge by single vertex index at index "+str(mesh["cur_vertex_pass"]))
if self.modify_mesh_advanced(context, mesh):
mesh["cur_vertex_pass"] = mesh["cur_vertex_pass"]+1
else:
print("finishing double merge object.")
if not self.advanced:
mesh["mesh"].select_set(True)
context.view_layer.objects.active = mesh["mesh"]
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action="INVERT")
bpy.ops.mesh.remove_doubles(threshold=self.merge_distance,use_unselected=False)
bpy.ops.object.mode_set(mode='OBJECT')
mesh["mesh"].select_set(False)
self.objects_to_do.pop(0)
else:
self.report({'INFO'}, t("Optimization.remove_doubles_completed"))
print("finishing modal execution of merge doubles safely.")
return {'FINISHED'}
return {'RUNNING_MODAL'}
-115
View File
@@ -1,115 +0,0 @@
import bpy
from ..core.register import register_wrap
from typing import List, Optional
import re
from bpy.types import Operator, Context, Object
from ..core.dictionaries import bone_names
from ..core.common import get_selected_armature, simplify_bonename, is_valid_armature
from ..functions.translations import t
@register_wrap
class AvatarToolKit_OT_ConvertToResonite(Operator):
bl_idname = 'avatar_toolkit.convert_to_resonite'
bl_label = t('Tools.convert_to_resonite.label')
bl_description = t('Tools.convert_to_resonite.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature)
def execute(self, context: Context) -> set:
armature = get_selected_armature(context)
if not armature:
self.report({'WARNING'}, t("Tools.no_armature_selected"))
return {'CANCELLED'}
translate_bone_fails = 0
untranslated_bones = set()
reverse_bone_lookup = dict()
for (preferred_name, name_list) in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
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.R",
'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"
}
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("",bone.name) #remove "NOIK" from bones before translating again, in case an update was done that fixes a translation.
for bone in armature.data.bones:
if simplify_bonename(bone.name) in reverse_bone_lookup and reverse_bone_lookup[simplify_bonename(bone.name)] in resonite_translations:
bone.name = resonite_translations[reverse_bone_lookup[simplify_bonename(bone.name)]]
else:
untranslated_bones.add(bone.name)
bone.name = bone.name+"<noik>"
translate_bone_fails += 1
bpy.ops.object.mode_set(mode='OBJECT')
if translate_bone_fails > 0:
self.report({'INFO'}, t("Tools.bones_translated_with_fails").format(translate_bone_fails=translate_bone_fails))
else:
self.report({'INFO'}, t("Tools.bones_translated_success"))
return {'FINISHED'}
-184
View File
@@ -1,184 +0,0 @@
# This code is heavily based on the Rigify-Move-DEF by NyankoNyan (https://github.com/NyankoNyan/Rigify-Move-DEF), which is licensed under the MIT License. We just heavily improve the code and add some new features.
import bpy
from ..core.register import register_wrap
from ..core.common import get_selected_armature, is_valid_armature
from ..functions.translations import t
from bpy.types import Operator, Context
import bpy
from ..core.register import register_wrap
from ..core.common import get_selected_armature, is_valid_armature
from ..functions.translations import t
from bpy.types import Operator, Context
@register_wrap
class AvatarToolKit_OT_ConvertRigifyToUnity(Operator):
bl_idname = "avatar_toolkit.convert_rigify_to_unity"
bl_label = t("Tools.convert_rigify_to_unity.label")
bl_description = t("Tools.convert_rigify_to_unity.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and "DEF-spine" in armature.data.bones
def execute(self, context: Context) -> set[str]:
armature = get_selected_armature(context)
if not armature:
self.report({'ERROR'}, t("Tools.no_armature_selected"))
return {'CANCELLED'}
self.move_def_bones(armature)
self.rename_bones_for_unity(armature)
if context.scene.merge_twist_bones:
self.handle_twist_bones(armature)
self.report({'INFO'}, t("Tools.convert_rigify_to_unity.success"))
return {'FINISHED'}
def move_def_bones(self, armature):
remap = self.get_org_remap(armature)
remap.update(self.get_special_remap())
remove_bones_in_chain = [
'DEF-upper_arm.L.001', 'DEF-forearm.L.001',
'DEF-upper_arm.R.001', 'DEF-forearm.R.001',
'DEF-thigh.L.001', 'DEF-shin.L.001',
'DEF-thigh.R.001', 'DEF-shin.R.001'
]
transform_copies = self.get_transform_copies(armature)
# Add missing constraints
bpy.ops.object.mode_set(mode='POSE')
for bone_name in transform_copies:
bone = armature.pose.bones[bone_name]
org_name = 'ORG-' + self.get_proto_name(bone_name)
if org_name in armature.pose.bones:
constraint = bone.constraints.new('COPY_TRANSFORMS')
constraint.target = armature
constraint.subtarget = org_name
constr_count = len(bone.constraints)
if constr_count > 1:
bone.constraints.move(constr_count-1, 0)
# Apply new parents
bpy.ops.object.mode_set(mode='EDIT')
for remap_key in remap:
if remap_key in armature.data.edit_bones and remap[remap_key] in armature.data.edit_bones:
armature.data.edit_bones[remap_key].parent = armature.data.edit_bones[remap[remap_key]]
# Remove extra bones in chains
bpy.ops.object.mode_set(mode='OBJECT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
armature.data.bones[bone_name].use_deform = False
bpy.ops.object.mode_set(mode='EDIT')
for bone_name in remove_bones_in_chain:
if bone_name in armature.data.bones:
remove_bone = armature.data.edit_bones[bone_name]
parent_bone = remove_bone.parent
parent_bone.tail = remove_bone.tail
retarget_bones = list(remove_bone.children)
for bone in retarget_bones:
bone.parent = parent_bone
armature.data.edit_bones.remove(remove_bone)
def rename_bones_for_unity(self, armature):
unity_bone_names = {
"DEF-spine": "Hips",
"DEF-spine.001": "Spine",
"DEF-spine.002": "Chest",
"DEF-spine.003": "UpperChest",
"DEF-neck": "Neck",
"DEF-head": "Head",
"DEF-shoulder.L": "LeftShoulder",
"DEF-upper_arm.L": "LeftUpperArm",
"DEF-forearm.L": "LeftLowerArm",
"DEF-hand.L": "LeftHand",
"DEF-shoulder.R": "RightShoulder",
"DEF-upper_arm.R": "RightUpperArm",
"DEF-forearm.R": "RightLowerArm",
"DEF-hand.R": "RightHand",
"DEF-thigh.L": "LeftUpperLeg",
"DEF-shin.L": "LeftLowerLeg",
"DEF-foot.L": "LeftFoot",
"DEF-toe.L": "LeftToes",
"DEF-thigh.R": "RightUpperLeg",
"DEF-shin.R": "RightLowerLeg",
"DEF-foot.R": "RightFoot",
"DEF-toe.R": "RightToes"
}
for old_name, new_name in unity_bone_names.items():
bone = armature.pose.bones.get(old_name)
if bone:
bone.name = new_name
def handle_twist_bones(self, armature):
twist_bones = [
("DEF-upper_arm_twist.L", "DEF-upper_arm.L"),
("DEF-upper_arm_twist.R", "DEF-upper_arm.R"),
("DEF-forearm_twist.L", "DEF-forearm.L"),
("DEF-forearm_twist.R", "DEF-forearm.R"),
("DEF-thigh_twist.L", "DEF-thigh.L"),
("DEF-thigh_twist.R", "DEF-thigh.R")
]
bpy.ops.object.mode_set(mode='EDIT')
for twist_bone, parent_bone in twist_bones:
if twist_bone in armature.data.edit_bones and parent_bone in armature.data.edit_bones:
twist = armature.data.edit_bones[twist_bone]
parent = armature.data.edit_bones[parent_bone]
parent.tail = twist.tail
for child in twist.children:
child.parent = parent
armature.data.edit_bones.remove(twist)
bpy.ops.object.mode_set(mode='OBJECT')
def get_org_remap(self, armature):
remap = {}
for bone in armature.data.bones:
if self.is_def_bone(bone.name):
name = self.get_proto_name(bone.name)
parent = bone.parent
while parent:
parent_name = self.get_proto_name(parent.name)
if parent_name != name:
if ('DEF-' + parent_name) in armature.data.bones:
remap[bone.name] = 'DEF-' + parent_name
break
parent = parent.parent
return remap
def get_special_remap(self):
return {
'DEF-thigh.L': 'DEF-pelvis.L',
'DEF-thigh.R': 'DEF-pelvis.R',
'DEF-upper_arm.L': 'DEF-shoulder.L',
'DEF-upper_arm.R': 'DEF-shoulder.R',
}
def get_transform_copies(self, armature):
result = []
for bone in armature.pose.bones:
if self.is_def_bone(bone.name) and not self.has_transform_copies(bone):
result.append(bone.name)
return result
def has_transform_copies(self, bone):
return any(constraint.type == 'COPY_TRANSFORMS' for constraint in bone.constraints)
def is_def_bone(self, bone_name):
return bone_name.startswith('DEF-')
def is_org_bone(self, bone_name):
return bone_name.startswith('ORG-')
def get_proto_name(self, bone_name):
if self.is_def_bone(bone_name) or self.is_org_bone(bone_name):
return bone_name[4:]
return bone_name
View File
+91
View File
@@ -0,0 +1,91 @@
import bpy
import numpy as np
from bpy.types import Operator, Context
from typing import Set
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, validate_armature, remove_unused_shapekeys
class AvatarToolkit_OT_ApplyTransforms(Operator):
"""Apply all transformations to armature and associated meshes"""
bl_idname = "avatar_toolkit.apply_transforms"
bl_label = t("Tools.apply_transforms")
bl_description = t("Tools.apply_transforms_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT'
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
logger.info(f"Applying transforms to {armature.name} and associated meshes")
# Select armature and meshes
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
meshes = get_all_meshes(context)
for mesh in meshes:
mesh.select_set(True)
# Apply transforms
bpy.ops.object.transform_apply(location=True, rotation=True, scale=True)
self.report({'INFO'}, t("Tools.transforms_applied"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply transforms: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_CleanShapekeys(Operator):
"""Remove unused shape keys from meshes"""
bl_idname = "avatar_toolkit.clean_shapekeys"
bl_label = t("Tools.clean_shapekeys")
bl_description = t("Tools.clean_shapekeys_desc")
bl_options = {'REGISTER', 'UNDO'}
tolerance: bpy.props.FloatProperty(
name=t("Tools.shapekey_tolerance"),
description=t("Tools.shapekey_tolerance_desc"),
default=0.001,
min=0.0001,
max=0.1
)
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting shape key cleanup")
removed_count = 0
for mesh in get_all_meshes(context):
if not mesh.data.shape_keys or not mesh.data.shape_keys.use_relative:
continue
removed = remove_unused_shapekeys(mesh, self.tolerance)
removed_count += removed
logger.debug(f"Removed {removed} shape keys from {mesh.name}")
self.report({'INFO'}, t("Tools.shapekeys_removed", count=removed_count))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to clean shape keys: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
+233
View File
@@ -0,0 +1,233 @@
import bpy
import re
from bpy.types import Operator, Context, EditBone, Object, Armature, Mesh
from typing import Optional, Dict, Any, List, Tuple
from ...core.translations import t
from ...core.common import (
get_active_armature,
validate_armature,
get_all_meshes,
ProgressTracker,
validate_bone_hierarchy,
restore_bone_transforms
)
def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone"""
arm = bone.id_data
new_bone = arm.edit_bones.new(bone.name + "_copy")
new_bone.head = bone.head
new_bone.tail = bone.tail
new_bone.roll = bone.roll
new_bone.parent = bone.parent
return new_bone
class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
"""Operator to convert standard legs to digitigrade setup"""
bl_idname = "avatar_toolkit.create_digitigrade"
bl_label = t("Tools.create_digitigrade")
bl_description = t("Tools.create_digitigrade_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return (is_valid and
context.mode == 'EDIT_ARMATURE' and
context.selected_editable_bones is not None and
len(context.selected_editable_bones) == 2)
def store_bone_chain_data(self, digi0: EditBone) -> Dict[str, Any]:
"""Store initial bone chain data"""
chain_data = {}
current = digi0
while current:
chain_data[current.name] = {
'head': current.head.copy(),
'tail': current.tail.copy(),
'roll': current.roll,
'matrix': current.matrix.copy(),
'parent': current.parent.name if current.parent else None
}
if current.children:
current = current.children[0]
else:
break
return chain_data
def process_leg_chain(self, digi0: EditBone) -> bool:
"""Process a single leg bone chain"""
try:
# Get bone chain
digi1: EditBone = digi0.children[0]
digi2: EditBone = digi1.children[0]
digi3: EditBone = digi2.children[0]
digi4: Optional[EditBone] = digi3.children[0] if digi3.children else None
# Clear roll for all bones
for bone in [digi0, digi1, digi2, digi3] + ([digi4] if digi4 else []):
bone.select = True
bpy.ops.armature.roll_clear()
bpy.ops.armature.select_all(action='DESELECT')
# Create thigh bone
thigh = duplicate_bone(digi0)
base_name = digi0.name.split('.')[0]
thigh.name = base_name
# Create and position calf bone
calf = duplicate_bone(digi1)
calf.name = digi1.name.split('.')[0]
calf.parent = thigh
# Calculate new positions
midpoint = (digi1.tail + digi2.tail) * 0.5
calf.head = thigh.tail
calf.tail = midpoint
# Reparent foot to new calf
digi3.parent = calf
# Mark original bones as non-IK
for bone in [digi0, digi1, digi2]:
if "<noik>" not in bone.name:
bone.name = bone.name.split('.')[0] + "<noik>"
return True
except Exception as e:
self.report({'ERROR'}, t("Tools.digitigrade_error", error=str(e)))
return False
def execute(self, context: Context) -> set[str]:
"""Execute the digitigrade conversion"""
bpy.ops.object.mode_set(mode='EDIT')
with ProgressTracker(context, len(context.selected_editable_bones), t("Tools.digitigrade")) as progress:
for digi0 in context.selected_editable_bones:
progress.step(t("Tools.processing_leg", bone=digi0.name))
if not self.process_leg_chain(digi0):
return {'CANCELLED'}
self.report({'INFO'}, t("Tools.digitigrade_success"))
return {'FINISHED'}
class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
"""Operator to remove all bone constraints from armature"""
bl_idname = "avatar_toolkit.clean_constraints"
bl_label = t("Tools.clean_constraints")
bl_description = t("Tools.clean_constraints_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> set[str]:
"""Execute the constraint removal operation"""
armature = get_active_armature(context)
# Select armature and make it active before changing mode
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='POSE')
constraints_removed = 0
for bone in armature.pose.bones:
while bone.constraints:
bone.constraints.remove(bone.constraints[0])
constraints_removed += 1
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.clean_constraints_success", count=constraints_removed))
return {'FINISHED'}
class AvatarToolKit_OT_RemoveZeroWeightBones(Operator):
"""Operator to remove bones with no vertex weights"""
bl_idname = "avatar_toolkit.clean_weights"
bl_label = t("Tools.clean_weights")
bl_description = t("Tools.clean_weights_desc")
bl_options = {'REGISTER', 'UNDO'}
def should_preserve_bone(self, bone_name: str, context: Context) -> bool:
"""Check if bone should be preserved based on settings"""
if context.scene.avatar_toolkit.merge_twist_bones:
return "twist" in bone_name.lower()
return False
def execute(self, context: Context) -> set[str]:
"""Execute the zero weight bone removal operation"""
armature = get_active_armature(context)
if not armature:
return {'CANCELLED'}
# Store initial transforms
bpy.ops.object.mode_set(mode='EDIT')
initial_transforms: Dict[str, Dict[str, Any]] = {}
for bone in armature.data.edit_bones:
initial_transforms[bone.name] = {
'head': bone.head.copy(),
'tail': bone.tail.copy(),
'roll': bone.roll,
'matrix': bone.matrix.copy(),
'parent': bone.parent.name if bone.parent else None
}
# Get weighted bones
weighted_bones: List[str] = []
meshes = get_all_meshes(context)
for mesh in meshes:
mesh_data: Mesh = mesh.data
for vertex in mesh_data.vertices:
for group in vertex.groups:
if group.weight > context.scene.avatar_toolkit.merge_weights_threshold:
weighted_bones.append(mesh.vertex_groups[group.group].name)
# Process bone removal
bpy.ops.object.mode_set(mode='EDIT')
armature_data: Armature = armature.data
removed_count = 0
for bone in armature_data.edit_bones[:]: # Create a copy of the list
if (bone.name not in weighted_bones and
not self.should_preserve_bone(bone.name, context)):
# Store children data
children = bone.children
children_data = {child.name: initial_transforms[child.name] for child in children}
# Reparent children
for child in children:
child.use_connect = False
if bone.parent:
child.parent = bone.parent
# Remove bone
armature_data.edit_bones.remove(bone)
removed_count += 1
# Restore children positions
for child_name, data in children_data.items():
if child_name in armature_data.edit_bones:
child = armature_data.edit_bones[child_name]
child.head = data['head']
child.tail = data['tail']
child.roll = data['roll']
child.matrix = data['matrix']
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.clean_weights_success", count=removed_count))
return {'FINISHED'}
+89
View File
@@ -0,0 +1,89 @@
import bpy
import re
from typing import Set, Dict, Optional
from bpy.types import Operator, Context
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker
from ...core.dictionaries import bone_names, resonite_translations
class AvatarToolkit_OT_ConvertResonite(Operator):
"""Convert armature bone names to Resonite format with progress tracking and validation"""
bl_idname = "avatar_toolkit.convert_resonite"
bl_label = t("Tools.convert_resonite")
bl_description = t("Tools.convert_resonite_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context)
if not armature:
logger.warning("No armature selected for Resonite conversion")
self.report({'WARNING'}, t("Armature.validation.no_armature"))
return {'CANCELLED'}
translate_bone_fails: int = 0
untranslated_bones: Set[str] = set()
simplified_names: Dict[str, str] = {}
# Create reverse lookup dictionary
reverse_bone_lookup = {}
for preferred_name, name_list in bone_names.items():
for name in name_list:
reverse_bone_lookup[name] = preferred_name
try:
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.object.mode_set(mode='OBJECT')
# Cache simplified bone names
for bone in armature.data.bones:
simplified_names[bone.name] = simplify_bonename(bone.name)
total_bones = len(armature.data.bones)
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
for bone in armature.data.bones:
# Remove any existing "<noik>" tags
bone.name = re.compile(re.escape("<noik>"), re.IGNORECASE).sub("", bone.name)
simplified_name = simplified_names[bone.name]
if simplified_name in reverse_bone_lookup and reverse_bone_lookup[simplified_name] in resonite_translations:
new_name = resonite_translations[reverse_bone_lookup[simplified_name]]
logger.debug(f"Translating bone: {bone.name} -> {new_name}")
bone.name = new_name
else:
untranslated_bones.add(bone.name)
bone.name = bone.name + "<noik>"
translate_bone_fails += 1
logger.debug(f"Failed to translate bone: {bone.name}")
progress.step(t("Tools.convert_resonite.processing", name=bone.name))
except Exception as e:
logger.error(f"Error during Resonite conversion: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
finally:
try:
bpy.ops.object.mode_set(mode='OBJECT')
except Exception as e:
logger.warning(f"Error returning to object mode: {str(e)}")
if translate_bone_fails > 0:
logger.info(f"Conversion completed with {translate_bone_fails} untranslated bones")
logger.debug(f"Untranslated bones: {untranslated_bones}")
self.report({'INFO'}, t("Tools.bones_translated_with_fails", translate_bone_fails=translate_bone_fails))
else:
logger.info("All bones translated successfully")
self.report({'INFO'}, t("Tools.bones_translated_success"))
return {'FINISHED'}
+161
View File
@@ -0,0 +1,161 @@
import bpy
import math
from typing import Set, List
from bpy.types import Operator, Context, Armature, EditBone
from ...core.translations import t
from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, validate_armature
class AvatarToolkit_OT_ConnectBones(Operator):
"""Connect disconnected bones in chain"""
bl_idname = "avatar_toolkit.connect_bones"
bl_label = t("Tools.connect_bones")
bl_description = t("Tools.connect_bones_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
logger.info("Starting bone connection operation")
bpy.ops.object.mode_set(mode='EDIT')
edit_bones = armature.data.edit_bones
bones_connected = 0
min_distance = context.scene.avatar_toolkit.connect_bones_min_distance
excluded_bones = {'LeftEye', 'RightEye', 'Head', 'Hips'}
for bone in edit_bones:
if len(bone.children) == 1 and bone.name not in excluded_bones:
child = bone.children[0]
distance = math.dist(bone.tail, child.head)
if distance > min_distance:
logger.debug(f"Connecting bone {bone.name} to {child.name}")
bone.tail = child.head
if bone.parent and len(bone.parent.children) == 1:
bone.use_connect = True
bones_connected += 1
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.connect_bones_success", count=bones_connected))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to connect bones: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToActive(Operator):
"""Merge selected bones into active bone and transfer weights"""
bl_idname = "avatar_toolkit.merge_to_active"
bl_label = t("Tools.merge_to_active")
bl_description = t("Tools.merge_to_active_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return context.mode == 'EDIT_ARMATURE' and context.active_bone
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
active_bone = context.active_bone
selected_bones = [b for b in context.selected_editable_bones if b != active_bone]
if not selected_bones:
self.report({'WARNING'}, t("Tools.no_bones_selected"))
return {'CANCELLED'}
logger.info(f"Merging {len(selected_bones)} bones into {active_bone.name}")
# Store weights before merging
meshes = get_all_meshes(context)
weight_data = {}
for bone in selected_bones:
for mesh in meshes:
if bone.name in mesh.vertex_groups:
weights = get_vertex_weights(mesh, bone.name)
weight_data.setdefault(mesh.name, {})[bone.name] = weights
# Transfer weights to active bone
threshold = context.scene.avatar_toolkit.merge_weights_threshold
for mesh_name, bone_weights in weight_data.items():
mesh = bpy.data.objects[mesh_name]
for bone_name, weights in bone_weights.items():
transfer_vertex_weights(mesh, bone_name, active_bone.name, threshold)
# Delete merged bones
for bone in selected_bones:
armature.data.edit_bones.remove(bone)
self.report({'INFO'}, t("Tools.merge_to_active_success", count=len(selected_bones)))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to merge bones: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolkit_OT_MergeToParent(Operator):
"""Merge selected bones into their respective parents and transfer weights"""
bl_idname = "avatar_toolkit.merge_to_parent"
bl_label = t("Tools.merge_to_parent")
bl_description = t("Tools.merge_to_parent_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
return context.mode == 'EDIT_ARMATURE'
def execute(self, context: Context) -> Set[str]:
try:
armature = get_active_armature(context)
selected_bones = [b for b in context.selected_editable_bones if b.parent]
if not selected_bones:
self.report({'WARNING'}, t("Tools.no_bones_with_parent"))
return {'CANCELLED'}
logger.info(f"Merging {len(selected_bones)} bones to their parents")
# Store weights before merging
meshes = get_all_meshes(context)
merged_count = 0
threshold = context.scene.avatar_toolkit.merge_weights_threshold
for bone in selected_bones:
parent = bone.parent
if not parent:
continue
# Transfer weights to parent
for mesh in meshes:
if bone.name in mesh.vertex_groups:
transfer_vertex_weights(mesh, bone.name, parent.name, threshold)
# Delete merged bone
armature.data.edit_bones.remove(bone)
merged_count += 1
self.report({'INFO'}, t("Tools.merge_to_parent_success", count=merged_count))
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to merge bones: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
+68
View File
@@ -0,0 +1,68 @@
import bpy
from bpy.types import Operator, Context
from ...core.translations import t
from ...core.common import get_active_armature, validate_armature
class AvatarToolKit_OT_SeparateByMaterials(Operator):
"""Operator to separate mesh by materials"""
bl_idname = "avatar_toolkit.separate_materials"
bl_label = t("Tools.separate_materials")
bl_description = t("Tools.separate_materials_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return (context.active_object and
context.active_object.type == 'MESH' and
is_valid)
def execute(self, context: Context) -> set[str]:
"""Execute the separation operation"""
try:
obj = context.active_object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.separate(type='MATERIAL')
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_materials_success"))
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
class AvatarToolKit_OT_SeparateByLooseParts(Operator):
"""Operator to separate mesh by loose parts"""
bl_idname = "avatar_toolkit.separate_loose"
bl_label = t("Tools.separate_loose")
bl_description = t("Tools.separate_loose_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if operator can be executed"""
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return (context.active_object and
context.active_object.type == 'MESH' and
is_valid)
def execute(self, context: Context) -> set[str]:
"""Execute the separation operation"""
try:
obj = context.active_object
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
bpy.ops.mesh.separate(type='LOOSE')
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, t("Tools.separate_loose_success"))
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
-97
View File
@@ -1,97 +0,0 @@
import os
import json
import bpy
from bpy.app.translations import locale
from typing import Dict, List, Tuple
from ..core.addon_preferences import save_preference, get_preference
# 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] = []
verbose: bool = True
def load_translations() -> bool:
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 = get_preference("language", 0)
# print(f"Loading translations for language index: {language_index}") # Debug print
if language_index == 0: # "auto"
language = bpy.context.preferences.view.language
else:
try:
language = languages[language_index]
except IndexError:
language = bpy.context.preferences.view.language
# print(f"Selected language: {language}") # Debug print
translation_file: str = os.path.join(translations_dir, language + ".json")
if os.path.exists(translation_file):
with open(translation_file, 'r', encoding='utf-8') as file:
dictionary = json.load(file)["messages"]
# print(f"Loaded translations: {dictionary}") # Debug print
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):
with open(custom_translation_file, 'r', encoding='utf-8') as file:
dictionary = json.load(file)["messages"]
# print(f"Loaded custom translations: {dictionary}") # Debug print
else:
print(f"Translation file not found for language: {language}")
default_file: str = os.path.join(translations_dir, "en_US.json")
if os.path.exists(default_file):
with open(default_file, 'r', encoding='utf-8') as file:
dictionary = json.load(file)["messages"]
# print(f"Loaded default translations: {dictionary}") # Debug print
else:
print("Default translation file 'en_US.json' not found.")
return dictionary != old_dictionary
def t(phrase: str, default: str = None, **kwargs) -> str:
output: str = dictionary.get(phrase)
if output is None:
if verbose:
print(f'Warning: Unknown phrase: {phrase}')
return default if default is not None else phrase
# print(f"Translating '{phrase}' to '{output}'") # Debug print
return output.format(**kwargs) if kwargs else output
def get_language_display_name(lang: str) -> str:
if lang == "auto":
return t("Language.auto", "Automatic")
return t(f"Language.{lang}", lang)
def get_languages_list(self, context) -> List[Tuple[str, str, str]]:
return [(str(i), get_language_display_name(lang), f"Use {lang} language") for i, lang in enumerate(languages)]
def update_language(self, context):
print(f"Updating language to: {self.avatar_toolkit_language}") # Debug print
save_preference("language", int(self.avatar_toolkit_language))
load_translations()
# Set a flag to indicate that a language change has occurred
context.scene.avatar_toolkit_language_changed = True
# Show popup after language change
bpy.ops.avatar_toolkit.translation_restart_popup('INVOKE_DEFAULT')
# Initial load of translations
# print("Performing initial load of translations") # Debug print
load_translations()
-296
View File
@@ -1,296 +0,0 @@
from typing import TypedDict
import bpy
from bpy.types import Operator, Object, Context, Mesh, MeshUVLoopLayer
import bmesh
import numpy as np
import math
from ..functions.translations import t
from ..core.register import register_wrap
class GenerateLoopTreeResult(TypedDict):
tree: dict[str, set[str]]
selected_loops: dict[str,list[int]]
selected_verts: dict[str,int]
@register_wrap
class AvatarToolkit_OT_AlignUVEdgesToTarget(Operator):
bl_idname = "avatar_toolkit.align_uv_edges_to_target"
bl_label = t("avatar_toolkit.align_uv_edges_to_target.label")
bl_description = t("avatar_toolkit.align_uv_edges_to_target.desc")
bl_options = {'REGISTER', 'UNDO'}
#all selected objects need to be meshes for this to work - @989onan
@classmethod
def poll(cls, context: Context):
if not ((context.view_layer.objects.active is not None) and (len(context.view_layer.objects.selected) > 0)):
return False
if context.mode != "EDIT_MESH":
return False
for obj in context.view_layer.objects.selected:
if obj.type != "MESH":
return False
if not context.space_data:
return False
if not context.space_data.show_uvedit:
return False
if context.scene.tool_settings.use_uv_select_sync:
return False
return True
def execute(self, context: Context):
target: str = context.view_layer.objects.active.name #The object which we want to align every other selected object's selected UV vertex line to
sources: list[str] = [i.name for i in context.view_layer.objects.selected] #The objects which we want to align their selected UV lines to the target's UV line
prev_mode: str = bpy.context.object.mode
bpy.ops.object.mode_set(mode='OBJECT')
def generate_loop_tree(obj_name: str) -> GenerateLoopTreeResult:
print("Finding selected line for: \""+obj_name+"\"!")
vert_target_loops: dict[str,list[int]] = {}
vert_target_verts: dict[str,int] = {}
me: Mesh = bpy.data.objects[obj_name].data
uv_lay: MeshUVLoopLayer = me.uv_layers.active
bm: bmesh.types.BMesh = bmesh.new()
bm.from_mesh(me)
bm.verts.ensure_lookup_table()
# To explain:
# So loops in UV maps are X polygons that make up a face (So a MeshLoop represent a face and each vertex on that face is in order)
#
# For some preknowledge:
# When a mesh is UV unwrapped, if a vertice is shared by two different faces on the model in the viewport and the vertice of both faces are in
# the same position on the UV map, then it considers it one point and the user can move it
# (is why the uv map doesn't split apart when you try to move a vertex because that would be annoying)
#
# The problem:
# The problem is that the data for whether the uv corners of two faces that share a vertex physically being connected and selected as one vertex on the uv map does not exist
# Though thankfully, blender forcibly (whether you like it or not) merges vertices of a uv map if the vertex of two different faces are actually shared in the UI,
# allowing for the moving of vertices of 4 faces connected by a single vertex. Behavior every normal blender user is familiar with.
#
# The solution
# We can use this to our advantage, by finding vertices on the uv map that share the same coridinate as another vertex that is also selected.
# that way we can group each pair shared in a line as the same vertex, and identify the line using these pairs and using the data that says for certain
# that two vertices share the same face loop, and therefore are connected.
#hmmm real stupid grimlin hours with this one. Using a string as the index of a dictionary of loop corners that end up on the same coordinate
for k,i in enumerate(uv_lay.vertex_selection): #go through the selected vertices on object.
if (i.value == True) and (bm.verts[me.loops[k].vertex_index].select == True) and (bm.verts[me.loops[k].vertex_index].hide == False): #filter out vertices that are hidden from UV port
key = np.array(uv_lay.uv[k].vector[:])
key = key.round(decimals=5) #make a key that is the position of a selected vertex
if str(key) not in vert_target_loops:
vert_target_loops[str(key)] = [] #if the vertex's position is not a list yet, add it.
vert_target_loops[str(key)].append(k) #Basically, group vertices based on their position on a UV map as a list.
vert_target_verts[str(key)] = me.loops[k].vertex_index #associate the index of the physical vertex in real space with the coordinate of the uv vertices that share a position (Basically associate UV vert with real vert)
if len(vert_target_loops) > 4000: #This usually indicates that the user has a bunch of crap selected.
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.too_much"))
return
print("Finding connections on line for \""+obj_name+"\"!")
me.validate()
bm = bmesh.new()
bm.from_mesh(me)
#print(vert_target_loops)
#print(vert_target_verts)
tree: dict[str, set[str]] = {}
selected_verts = np.hstack(list(vert_target_loops.values()))
#print(selected_verts)
bm.verts.ensure_lookup_table()
for uvcoordsstr in vert_target_loops:
uv_lay = me.uv_layers.active
#before this section, each vert_target_loops is just groupings of vertices that share coordinates.
# Using the data that determines UV face corners (uvloops) that are associated with the real vertex,
# and the uv face corners (loops) that are on the same faces as the vertices that share coordinates in
# vert_target_loops, we can now identify them
#TL;DR: pairs of vertices that share cooridinates (chain links) find their buddies (make chain connected)
# Someone explain this better than me if you can please - @989onan
extension_loops = []
loops = bm.verts[vert_target_verts[uvcoordsstr]].link_loops
loops_indexes = [i.index for i in loops]
for loop in vert_target_loops[uvcoordsstr]:
if loop in loops_indexes:
loop_obj = loops[loops_indexes.index(loop)]
extension_loops.append(loop_obj.link_loop_next.index)
extension_loops.append(loop_obj.link_loop_prev.index)
#make a tree out of the vertices we identified as sharing faces with the vertices in vert_target_loops, and then link them together in a dictionary.
#the order of this dictionary is unknown.
# Someone explain this better than me if you can please - @989onan
tree[uvcoordsstr] = set()
for i in extension_loops:
if i in selected_verts:
key = np.array(uv_lay.uv[i].vector[:])
key = key.round(decimals=5)
tree[uvcoordsstr].add(str(key))
if uvcoordsstr in tree:
if len(tree[uvcoordsstr]) > 2:
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
return {'FINISHED'}
uv_lay = me.uv_layers.active
for uvcoordstr in vert_target_loops:
for loop in vert_target_loops[uvcoordstr]:
uv_lay.vertex_selection[loop].value = True
bm.free()
me.validate()
print("found UV line connections for \""+obj_name+"\":")
#print(tree)
return {"tree":tree,"selected_loops":vert_target_loops,"selected_verts":vert_target_verts}
#This function uses the previous point to find the next point based on connected loops and faces.
def sort_uv_tree(originaltree: dict[str, set[str]], obj_name: str):
sortedtree: dict[str, set[str]] = originaltree.copy()
startpoints: list[str] = []
for i in sortedtree:
if len(sortedtree[i]) < 2:
startpoints.append(i)
if len(startpoints) != 2:
self.report({'WARNING'}, t("UVTools.align_uv_to_target.warning.need_a_line").format(obj=obj_name))
return
a_list1 = startpoints[0].replace(", "," ").replace("[","").replace("]","").split()
map_object1 = map(float, a_list1)
uvcoords1 = list(map_object1)
a_list2 = startpoints[1].replace(", "," ").replace("[","").replace("]","").split()
map_object2 = map(float, a_list2)
uvcoords2 = list(map_object2)
cursor = context.space_data.cursor_location
startpoint = None
if math.sqrt( (((uvcoords1[0]) - (cursor[0])) **2) + (((uvcoords1[1]) - (cursor[1])) **2) ) > math.sqrt( (((uvcoords2[0]) - (cursor[0])) **2) + (((uvcoords2[1]) - (cursor[1])) **2) ):
startpoint = startpoints[0]
else:
startpoint = startpoints[1]
#Wew my first actual recursive sort! - @989onan
def recursive_sort_uv_tree(point: str, sortedfinal: list[str]):
#print("appending "+point)
sortedfinal.append(point)
new_point: str = ""
for i in sortedtree:
if point in sortedtree[i]:
new_point = i
removed_value = sortedtree.pop(i)
#print(removed_value)
break
if new_point == "":
print("BROKE OUT OF SORTING, FINAL TREE (Should be empty, if not you errored here!):")
print(sortedtree)
return sortedfinal
return recursive_sort_uv_tree(new_point, sortedfinal)
array = []
sortedtree.pop(startpoint)
return recursive_sort_uv_tree(startpoint, array)
def lerp(v0, v1, t):
return v0 + t * (v1 - v0)
target_data: GenerateLoopTreeResult = generate_loop_tree(target)
sorted_target_tree = sort_uv_tree(target_data["tree"], target)
print("sorted target.")
#print(sorted_target_tree)
for source in sources:
if source == target:
continue
#create our list of points that is a chain. then sort the chain into the correct order based on connections of vertices and the faces that the vertices make up in the UV map.
try:
source_data = generate_loop_tree(source)
sorted_source_tree = sort_uv_tree(source_data["tree"], source)
print("Sorted source "+source)
print(sorted_source_tree)
vertex_factor = float(len(sorted_target_tree)-1) / (float(len(sorted_source_tree)-1))
print(str(vertex_factor)+" = "+str(float(len(sorted_target_tree)-1)) + " / " + str((float(len(sorted_source_tree)-1)))+")")
except Exception as e:
print(e)
return {'FINISHED'}
for k,i in enumerate(sorted_source_tree):
try:
#find where we are on the target edges, to interpolate the current point we're placing along the target point's line.
progress_along_edge = (float(k)*vertex_factor)
previous_vertex_index = math.floor(progress_along_edge)
next_vertex_index = math.ceil(progress_along_edge)
#find the uv coordinates of the previous and next points on the target uv line.
a_list1 = sorted_target_tree[previous_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
map_object1 = map(float, a_list1)
previous_point = list(map_object1)
a_list2 = sorted_target_tree[next_vertex_index].replace(", "," ").replace("[","").replace("]","").split()
map_object2 = map(float, a_list2)
next_point = list(map_object2)
#create a point between these two values that represents a decimal 0-1 going where we are to where we are going between the two current points on the edge we are targeting this whole shebang with.
progress_between_points = progress_along_edge - int(progress_along_edge)
lerped_point = [lerp(previous_point[0],next_point[0],progress_between_points),lerp(previous_point[1],next_point[1],progress_between_points)]
#grab our uv face corners for each uv coord that we saved.
#Since each face is considered separate internally, we have to treat each connected face to a vertex in a uv map as separate entities/vertexes.
#basically pretend they are split apart.
uv_face_corners = source_data["selected_loops"][i]
#print("doing from vertex "+str(previous_vertex_index)+" to "+str(next_vertex_index)+" total progress: "+str(progress_along_edge))
me: Mesh = bpy.data.objects[source].data
me.validate()
bm: bmesh.types.BMesh = bmesh.new()
bm.from_mesh(me)
uv_lay: MeshUVLoopLayer = me.uv_layers.active
bm.verts.ensure_lookup_table()
for corner in uv_face_corners:
uv_lay.uv[corner].vector = lerped_point #put the vertcies at the point we calculated.
except:
print("This is probably fine? - @989onan") #TODO: What happened here? The magic of making code so complex you forget if this is even an issue. - @989onan
print("Finished mesh \""+source+"\" for UV's")
bpy.ops.object.mode_set(mode=prev_mode)
return {'FINISHED'}
-94
View File
@@ -1,94 +0,0 @@
import bpy
from ..core import common
from ..core.register import register_wrap
from ..functions.translations import t
from typing import List, Tuple
from ..core.common import get_selected_armature, is_valid_armature, get_all_meshes, init_progress, update_progress, finish_progress
@register_wrap
class AvatarToolKit_OT_AutoVisemeButton(bpy.types.Operator):
bl_idname = 'avatar_toolkit.create_visemes'
bl_label = t('AutoVisemeButton.label')
bl_description = t('AutoVisemeButton.desc')
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
armature = get_selected_armature(context)
return armature is not None and is_valid_armature(armature) and get_all_meshes(context)
def execute(self, context: bpy.types.Context) -> set:
try:
self.create_visemes(context)
return {'FINISHED'}
except Exception as e:
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def create_visemes(self, context: bpy.types.Context) -> None:
init_progress(context, 5) # 5 main steps
update_progress(self, context, t("VisemePanel.start_viseme_creation"))
mesh = bpy.data.objects.get(context.scene.selected_mesh)
if not mesh or not common.has_shapekeys(mesh):
raise ValueError(t('AutoVisemeButton.error.noShapekeys'))
update_progress(self, context, t("VisemePanel.removing_existing_visemes"))
self.remove_existing_vrc_shapekeys(mesh)
shape_a = context.scene.avatar_toolkit_mouth_a
shape_o = context.scene.avatar_toolkit_mouth_o
shape_ch = context.scene.avatar_toolkit_mouth_ch
if shape_a == "Basis" or shape_o == "Basis" or shape_ch == "Basis":
raise ValueError(t('AutoVisemeButton.error.selectShapekeys'))
update_progress(self, context, t("VisemePanel.creating_visemes"))
visemes: List[Tuple[str, List[Tuple[str, float]]]] = [
('vrc.v_aa', [(shape_a, 0.9998)]),
('vrc.v_ch', [(shape_ch, 0.9996)]),
('vrc.v_dd', [(shape_a, 0.3), (shape_ch, 0.7)]),
('vrc.v_e', [(shape_a, 0.5), (shape_ch, 0.2)]),
('vrc.v_ff', [(shape_a, 0.2), (shape_ch, 0.4)]),
('vrc.v_ih', [(shape_ch, 0.7), (shape_o, 0.3)]),
('vrc.v_kk', [(shape_a, 0.7), (shape_ch, 0.4)]),
('vrc.v_nn', [(shape_a, 0.2), (shape_ch, 0.7)]),
('vrc.v_oh', [(shape_a, 0.2), (shape_o, 0.8)]),
('vrc.v_ou', [(shape_o, 0.9994)]),
('vrc.v_pp', [(shape_a, 0.0004), (shape_o, 0.0004)]),
('vrc.v_rr', [(shape_ch, 0.5), (shape_o, 0.3)]),
('vrc.v_sil', [(shape_a, 0.0002), (shape_ch, 0.0002)]),
('vrc.v_ss', [(shape_ch, 0.8)]),
('vrc.v_th', [(shape_a, 0.4), (shape_o, 0.15)])
]
for viseme_name, shape_mix in visemes:
self.create_viseme(mesh, viseme_name, shape_mix, context.scene.avatar_toolkit_shape_intensity)
update_progress(self, context, t("VisemePanel.sorting_shapekeys"))
common.sort_shape_keys(mesh)
update_progress(self, context, t("VisemePanel.viseme_creation_completed"))
finish_progress(context)
def create_viseme(self, mesh: bpy.types.Object, viseme_name: str, shape_mix: List[Tuple[str, float]], intensity: float) -> None:
shape_keys = mesh.data.shape_keys.key_blocks
if viseme_name in shape_keys:
mesh.shape_key_remove(shape_keys[viseme_name])
new_key = mesh.shape_key_add(name=viseme_name, from_mix=False)
new_key.value = 0.0
for shape_name, value in shape_mix:
if shape_name in shape_keys:
source_shape = shape_keys[shape_name]
for i, vert in enumerate(new_key.data):
vert.co += (source_shape.data[i].co - shape_keys['Basis'].data[i].co) * value * intensity
def remove_existing_vrc_shapekeys(self, mesh: bpy.types.Object) -> None:
vrc_prefixes = ['vrc.v_', 'vrc.blink_', 'vrc.lowerlid_']
shape_keys = mesh.data.shape_keys.key_blocks
for key in reversed(shape_keys):
if any(key.name.startswith(prefix) for prefix in vrc_prefixes):
mesh.shape_key_remove(key)
+335
View File
@@ -0,0 +1,335 @@
# MIT License
# This code was taken from Cats Blender Plugin Unoffical, some of this code is by the original developers, however was improved by myself.
# Didn't think it was necessary to re-make something that works well.
import bpy
from typing import Dict, List, Optional, Tuple, Any, Set
from bpy.types import Operator, Context, Object, ShapeKey
from collections import OrderedDict
from ..core.logging_setup import logger
from ..core.translations import t
from ..core.common import (
get_active_armature,
validate_armature,
get_all_meshes,
validate_mesh_for_pose
)
class VisemeCache:
"""Caches generated viseme shape data"""
_cache: Dict = {}
@classmethod
def get_cached_shape(cls, key: str, mix_data: List) -> Optional[List]:
cache_key = (key, tuple(tuple(x) for x in mix_data))
return cls._cache.get(cache_key)
@classmethod
def cache_shape(cls, key: str, mix_data: List, shape_data: List) -> None:
cache_key = (key, tuple(tuple(x) for x in mix_data))
cls._cache[cache_key] = shape_data
class VisemePreview:
"""Handles viseme preview functionality"""
_preview_data: Dict = {}
_active: bool = False
_preview_shapes: Optional[OrderedDict] = None
@classmethod
def start_preview(cls, context: Context, mesh: Object, shapes: List[str]) -> bool:
if not mesh or not mesh.data or not mesh.data.shape_keys:
return False
cls._active = True
cls._preview_data = {}
# Store original values
for shape_key in mesh.data.shape_keys.key_blocks:
cls._preview_data[shape_key.name] = shape_key.value
# Get properties from avatar_toolkit
props = context.scene.avatar_toolkit
shape_a = props.mouth_a
shape_o = props.mouth_o
shape_ch = props.mouth_ch
cls._preview_shapes = OrderedDict()
cls._preview_shapes['vrc.v_aa'] = {'mix': [[(shape_a), (0.9998)]]}
cls._preview_shapes['vrc.v_ch'] = {'mix': [[(shape_ch), (0.9996)]]}
cls._preview_shapes['vrc.v_dd'] = {'mix': [[(shape_a), (0.3)], [(shape_ch), (0.7)]]}
cls._preview_shapes['vrc.v_ih'] = {'mix': [[(shape_ch), (0.7)], [(shape_o), (0.3)]]}
cls._preview_shapes['vrc.v_ff'] = {'mix': [[(shape_a), (0.2)], [(shape_ch), (0.4)]]}
cls._preview_shapes['vrc.v_e'] = {'mix': [[(shape_a), (0.5)], [(shape_ch), (0.2)]]}
cls._preview_shapes['vrc.v_kk'] = {'mix': [[(shape_a), (0.7)], [(shape_ch), (0.4)]]}
cls._preview_shapes['vrc.v_nn'] = {'mix': [[(shape_a), (0.2)], [(shape_ch), (0.7)]]}
cls._preview_shapes['vrc.v_oh'] = {'mix': [[(shape_a), (0.2)], [(shape_o), (0.8)]]}
cls._preview_shapes['vrc.v_ou'] = {'mix': [[(shape_o), (0.9994)]]}
cls._preview_shapes['vrc.v_pp'] = {'mix': [[(shape_a), (0.0004)], [(shape_o), (0.0004)]]}
cls._preview_shapes['vrc.v_rr'] = {'mix': [[(shape_ch), (0.5)], [(shape_o), (0.3)]]}
cls._preview_shapes['vrc.v_sil'] = {'mix': [[(shape_a), (0.0002)], [(shape_ch), (0.0002)]]}
cls._preview_shapes['vrc.v_ss'] = {'mix': [[(shape_ch), (0.8)]]}
cls._preview_shapes['vrc.v_th'] = {'mix': [[(shape_a), (0.4)], [(shape_o), (0.15)]]}
return True
@classmethod
def update_preview(cls, context: Context) -> None:
if not cls._active or not cls._preview_shapes:
return
mesh = context.active_object
props = context.scene.avatar_toolkit
viseme_data = cls._preview_shapes.get(props.viseme_preview_selection)
if viseme_data:
cls.show_viseme(context, mesh, props.viseme_preview_selection, viseme_data['mix'])
@classmethod
def show_viseme(cls, context: Context, mesh: Object, viseme_name: str, mix_data: List) -> None:
if not cls._active:
return
# Get shape intensity from properties
intensity = context.scene.avatar_toolkit.shape_intensity
for shape_key in mesh.data.shape_keys.key_blocks:
shape_key.value = 0
for shape_name, value in mix_data:
if shape_name in mesh.data.shape_keys.key_blocks:
# Apply intensity to the preview value
mesh.data.shape_keys.key_blocks[shape_name].value = value * intensity
context.view_layer.update()
@classmethod
def end_preview(cls, mesh: Object) -> None:
if not cls._active:
return
for shape_name, value in cls._preview_data.items():
if shape_name in mesh.data.shape_keys.key_blocks:
mesh.data.shape_keys.key_blocks[shape_name].value = value
cls._active = False
cls._preview_data.clear()
cls._preview_shapes = None
class ATOOLKIT_OT_preview_visemes(Operator):
bl_idname = "avatar_toolkit.preview_visemes"
bl_label = t("Visemes.preview_label")
bl_description = t("Visemes.preview_desc")
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid and context.active_object and context.active_object.type == 'MESH'
def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit
mesh = context.active_object
if props.viseme_preview_mode:
VisemePreview.end_preview(mesh)
props.viseme_preview_mode = False
else:
if not mesh.data.shape_keys:
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
return {'CANCELLED'}
if VisemePreview.start_preview(context, mesh, [props.mouth_a, props.mouth_o, props.mouth_ch]):
props.viseme_preview_mode = True
props.viseme_preview_selection = 'vrc.v_aa'
return {'FINISHED'}
def validate_deformation(mesh, mix_data):
"""Validates if shape key deformations are within reasonable ranges"""
base_coords = [v.co.copy() for v in mesh.data.shape_keys.key_blocks['Basis'].data]
max_deform = 0
for shape_data in mix_data:
shape_name, value = shape_data
if shape_name in mesh.data.shape_keys.key_blocks:
shape_key = mesh.data.shape_keys.key_blocks[shape_name]
for i, v in enumerate(shape_key.data):
deform = (v.co - base_coords[i]).length * value
max_deform = max(max_deform, deform)
mesh_size = max(mesh.dimensions)
return max_deform < (mesh_size * 0.4)
class ATOOLKIT_OT_create_visemes(Operator):
bl_idname = "avatar_toolkit.create_visemes"
bl_label = t("Visemes.create_label")
bl_description = t("Visemes.create_desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid and context.active_object and context.active_object.type == 'MESH'
def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit
mesh = context.active_object
if not mesh.data.shape_keys:
self.report({'ERROR'}, t("Visemes.error.no_shapekeys"))
return {'CANCELLED'}
if props.mouth_a == "Basis" or props.mouth_o == "Basis" or props.mouth_ch == "Basis":
self.report({'ERROR'}, t("Visemes.error.select_shapekeys"))
return {'CANCELLED'}
try:
self.create_visemes(context, mesh)
self.report({'INFO'}, t("Visemes.success"))
return {'FINISHED'}
except Exception as e:
logger.error(f"Error creating visemes: {str(e)}")
self.report({'ERROR'}, str(e))
return {'CANCELLED'}
def create_visemes(self, context: Context, mesh: Object) -> None:
"""Creates viseme shape keys by mixing existing shape keys"""
props = context.scene.avatar_toolkit
wm = context.window_manager
# Store original shape key names
shapes = [props.mouth_a, props.mouth_o, props.mouth_ch]
renamed_shapes = shapes.copy()
# Temporarily rename selected shapes to avoid conflicts
for shapekey in mesh.data.shape_keys.key_blocks:
if shapekey.name == props.mouth_a:
shapekey.name = f"{shapekey.name}_old"
props.mouth_a = shapekey.name
renamed_shapes[0] = shapekey.name
elif shapekey.name == props.mouth_o:
if props.mouth_a != props.mouth_o:
shapekey.name = f"{shapekey.name}_old"
props.mouth_o = shapekey.name
renamed_shapes[1] = shapekey.name
elif shapekey.name == props.mouth_ch:
if props.mouth_a != props.mouth_ch and props.mouth_o != props.mouth_ch:
shapekey.name = f"{shapekey.name}_old"
props.mouth_ch = shapekey.name
renamed_shapes[2] = shapekey.name
# Define viseme shape key data
shapekey_data = OrderedDict()
shapekey_data['vrc.v_aa'] = {'mix': [[(props.mouth_a), (0.9998)]]}
shapekey_data['vrc.v_ch'] = {'mix': [[(props.mouth_ch), (0.9996)]]}
shapekey_data['vrc.v_dd'] = {'mix': [[(props.mouth_a), (0.3)], [(props.mouth_ch), (0.7)]]}
shapekey_data['vrc.v_ih'] = {'mix': [[(props.mouth_ch), (0.7)], [(props.mouth_o), (0.3)]]}
shapekey_data['vrc.v_ff'] = {'mix': [[(props.mouth_a), (0.2)], [(props.mouth_ch), (0.4)]]}
shapekey_data['vrc.v_e'] = {'mix': [[(props.mouth_a), (0.5)], [(props.mouth_ch), (0.2)]]}
shapekey_data['vrc.v_kk'] = {'mix': [[(props.mouth_a), (0.7)], [(props.mouth_ch), (0.4)]]}
shapekey_data['vrc.v_nn'] = {'mix': [[(props.mouth_a), (0.2)], [(props.mouth_ch), (0.7)]]}
shapekey_data['vrc.v_oh'] = {'mix': [[(props.mouth_a), (0.2)], [(props.mouth_o), (0.8)]]}
shapekey_data['vrc.v_ou'] = {'mix': [[(props.mouth_o), (0.9994)]]}
shapekey_data['vrc.v_pp'] = {'mix': [[(props.mouth_a), (0.0004)], [(props.mouth_o), (0.0004)]]}
shapekey_data['vrc.v_rr'] = {'mix': [[(props.mouth_ch), (0.5)], [(props.mouth_o), (0.3)]]}
shapekey_data['vrc.v_sil'] = {'mix': [[(props.mouth_a), (0.0002)], [(props.mouth_ch), (0.0002)]]}
shapekey_data['vrc.v_ss'] = {'mix': [[(props.mouth_ch), (0.8)]]}
shapekey_data['vrc.v_th'] = {'mix': [[(props.mouth_a), (0.4)], [(props.mouth_o), (0.15)]]}
# Create progress tracker
total_steps = len(shapekey_data)
wm.progress_begin(0, total_steps)
# Create viseme shape keys
for index, (key, data) in enumerate(shapekey_data.items()):
wm.progress_update(index)
# Check cache first
cached_data = VisemeCache.get_cached_shape(key, data['mix'])
if cached_data:
continue
# Create new shape key
self.mix_shapekey(context, renamed_shapes, data['mix'], key)
# Cache the new shape key data
shape_data = [v.co.copy() for v in mesh.data.shape_keys.key_blocks[key].data]
VisemeCache.cache_shape(key, data['mix'], shape_data)
# Restore original shape key names
self.restore_shape_names(context, mesh, shapes, renamed_shapes)
# Cleanup and finalize
mesh.active_shape_key_index = 0
wm.progress_end()
def mix_shapekey(self, context: Context, shapes: List[str], mix_data: List, new_name: str) -> None:
"""Creates a new shape key by mixing existing ones"""
mesh = context.active_object
# Remove existing shape key if it exists
if new_name in mesh.data.shape_keys.key_blocks:
mesh.active_shape_key_index = mesh.data.shape_keys.key_blocks.find(new_name)
bpy.ops.object.shape_key_remove()
# Reset all shape keys
for shapekey in mesh.data.shape_keys.key_blocks:
shapekey.value = 0
# Set mix values
for shape_name, value in mix_data:
if shape_name in mesh.data.shape_keys.key_blocks:
shapekey = mesh.data.shape_keys.key_blocks[shape_name]
shapekey.value = value
# Create mixed shape key
mesh.shape_key_add(name=new_name, from_mix=True)
# Reset values and restore shape key settings
for shapekey in mesh.data.shape_keys.key_blocks:
shapekey.value = 0
if shapekey.name in shapes:
shapekey.slider_max = 1
def restore_shape_names(self, context: Context, mesh: Object, original_names: List[str], current_names: List[str]) -> None:
"""Restores original shape key names"""
props = context.scene.avatar_toolkit
# Restore mouth_a
if original_names[0] not in mesh.data.shape_keys.key_blocks:
shapekey = mesh.data.shape_keys.key_blocks.get(current_names[0])
if shapekey:
shapekey.name = original_names[0]
if current_names[2] == current_names[0]:
current_names[2] = original_names[0]
if current_names[1] == current_names[0]:
current_names[1] = original_names[0]
current_names[0] = original_names[0]
# Restore mouth_o
if original_names[1] not in mesh.data.shape_keys.key_blocks:
shapekey = mesh.data.shape_keys.key_blocks.get(current_names[1])
if shapekey:
shapekey.name = original_names[1]
if current_names[2] == current_names[1]:
current_names[2] = original_names[1]
current_names[1] = original_names[1]
# Restore mouth_ch
if original_names[2] not in mesh.data.shape_keys.key_blocks:
shapekey = mesh.data.shape_keys.key_blocks.get(current_names[2])
if shapekey:
shapekey.name = original_names[2]
current_names[2] = original_names[2]
# Update properties
props.mouth_a = current_names[0]
props.mouth_o = current_names[1]
props.mouth_ch = current_names[2]
+379 -276
View File
@@ -1,269 +1,18 @@
{
"authors": ["Avatar Toolkit Team"],
"messages": {
"AutoVisemeButton.desc": "Create visemes automatically, based on shape keys",
"AutoVisemeButton.error.noShapekeys": "No shape keys found",
"AutoVisemeButton.error.selectShapekeys": "Please Select shape keys",
"AutoVisemeButton.label": "Create Visemes",
"AutoVisemeButton.success": "Visemes created successfully",
"AvatarToolkit.label": "Avatar Toolkit (Alpha)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access",
"AvatarToolkit.desc2": "There will be issues, if you find",
"AvatarToolkit.desc3": "an issue, please report it on Github.",
"Export.resonite.desc": "Export a GLB with all animations and materials. For animation data see:",
"Export.resonite.label": "Export to Resonite",
"Importer.export_resonite.desc": "Export to Resonite as a GLTF. Make sure your model is to scale in blender, and import as meters in Resonite.",
"Importer.export_resonite.label": "Export to Resonite",
"Importer.export_vrchat.desc": "Export to VRChat, may also work for ChilloutVR. Is similar to Cats export.",
"Importer.export_vrchat.label": "Export to VRChat",
"Importer.mmd_anim_importer.desc": "Import a MMD Animation (.vmd)",
"Importer.mmd_anim_importer.label": "MMD Animation",
"Importing.importer_search_term": "https://search.brave.com/search?q=blender+{extension}+importer+addon&source=web",
"Importing.need_importer": "You do not have the required importer for the {extension} type! Opening web browser for importer search term...",
"Language.auto": "Automatic",
"Language.en_US": "English",
"Language.ja_JP": "日本語",
"Optimization.applying_transforms": "Applying transforms...",
"Optimization.cleaning_material_names": "Cleaning material names...",
"Optimization.cleaning_material_slots": "Cleaning material slots...",
"Optimization.clearing_unused_data": "Clearing unused data...",
"Optimization.materials_optimization_report": "Materials optimization completed: Combined {num_combined} materials, cleaned {num_cleaned_slots} material slots, cleaned {num_cleaned_names} material names, and removed {num_removed_data_blocks} unused data blocks",
"Optimization.combine_materials.desc": "Combine similar materials to reduce draw calls and improve performance",
"Optimization.combine_materials.label": "Combine Materials",
"Optimization.consolidating_materials": "Consolidating materials...",
"Optimization.finalizing": "Finalizing...",
"Optimization.fixing_uv_coordinates": "Fixing UV coordinates...",
"Optimization.join_all_meshes.desc": "Merge all meshes into a single object to reduce draw calls",
"Optimization.join_all_meshes.label": "Join All Meshes",
"Optimization.join_error": "Error during mesh joining",
"Optimization.join_operation_failed": "Join operation failed",
"Optimization.join_selected_meshes.desc": "Merge only the selected meshes into a single object",
"Optimization.join_selected_meshes.label": "Join Selected Meshes",
"Optimization.joinmeshes.label": "Join Meshes:",
"Optimization.joining_meshes": "Joining meshes...",
"Optimization.label": "Optimization",
"Optimization.material_attribute_mismatch": "Attribute mismatch in material {material_name}, skipping",
"Optimization.materials_combined": "Combined {num_combined} materials",
"Optimization.meshes_joined": "Meshes joined successfully",
"Optimization.no_armature_selected": "No armature selected",
"Optimization.no_mesh_selected": "No mesh objects selected",
"Optimization.no_meshes_found": "No meshes found for the selected armature",
"Optimization.options.label": "Optimization:",
"Optimization.preparing_meshes": "Preparing meshes...",
"Optimization.processing_mesh_no_shapekeys": "Processing mesh with no shapekeys named \"{mesh_name}\"",
"Optimization.processing_shapekey": "Processing shapekey \"{shapekeyname}\" on mesh \"{mesh_name}\"",
"Optimization.remove_doubles_completed": "Remove doubles operation completed",
"Optimization.remove_doubles_safely.desc": "Remove duplicate vertices while preserving important features like mouth shapes.\nIs a quick solution but does not merge vertices that move at all.",
"Optimization.remove_doubles_safely.label": "Remove Doubles Safely",
"Optimization.remove_doubles_safely_advanced.label": "Advanced Remove Doubles Safely",
"Optimization.remove_doubles_safely_advanced.desc": "Remove duplicate vertices while preserving important features like mouth shapes.\nUnlike basic, Advanced will merge vertices together that move, but still preserve shapekeys.\nEx: It will not seal the lips of the mouth closed, but will fix split polygons that make up the lips.",
"Optimization.select_armature": "Please select an armature",
"Optimization.select_at_least_two_meshes": "Please select at least two mesh objects",
"Optimization.selected_meshes_joined": "Selected meshes joined successfully",
"Optimization.selecting_meshes": "Selecting meshes...",
"Optimization.transform_apply_failed": "Transform apply failed",
"Optimization.vertex_excluded": "Shapekey has a moved vertex at index \"{index}\", excluding from double merging!",
"Quick_Access.selected_armature.label": "Selected Armature",
"Quick_Access.selected_armature.desc": "The currently \"targeted\" armature for Avatar Toolkit operations",
"Quick_Access.export": "Export",
"Quick_Access.export_fbx.desc": "Export the model as FBX",
"Quick_Access.export_fbx.label": "Export FBX",
"Quick_Access.export_menu.desc": "Export to a supported format",
"Quick_Access.export_menu.label": "Export Menu",
"Quick_Access.import": "Import",
"Quick_Access.import_export.label": "Import/Export:",
"Quick_Access.import_menu.desc": "Import a Model",
"Quick_Access.import_menu.label": "Import Menu",
"Quick_Access.import_pmd": "Import PMD",
"Quick_Access.import_pmd.desc": "Import MMD PMD Model",
"Quick_Access.import_pmx": "Import PMX",
"Quick_Access.import_pmx.desc": "Import MMD PMX Model",
"Quick_Access.import_success": "Model imported successfully",
"Quick_Access.label": "Quick Access",
"Quick_Access.options": "Quick Access:",
"Quick_Access.select_armature": "Select Armature:",
"Quick_Access.apply_armature_failed": "Applying armature as pose failed at the joining shapekeys back together stage!",
"Quick_Access.apply_pose_as_rest.desc": "Makes current pose the default rest pose.",
"Quick_Access.stop_pose_mode.desc": "Exits pose mode and clears all posing on all visible bones in pose mode.",
"Quick_Access.apply_pose_as_rest.label": "Apply Pose as Rest Pose",
"Quick_Access.apply_pose_as_shapekey.desc": "Makes the current pose a shapekey that can be activated later.\nThis is good for applying a jaw open position as a shapekey for facial movements.",
"Quick_Access.apply_pose_as_shapekey.label": "Apply Pose as Shapekey",
"Quick_Access.stop_pose_mode.label": "Exit Pose Mode",
"Quick_Access.start_pose_mode.desc": "Starts pose mode for the armature targeted by Avatar Toolkit.",
"Quick_Access.start_pose_mode.label": "Start Pose Mode",
"Quick_Access.select_export.label": "Select Export Method",
"Quick_Access.select_export_resonite.label": "Resonite",
"Settings.label": "Settings",
"Settings.language.desc": "Select the language for the addon's UI",
"Settings.language.label": "Language:",
"Settings.translation_restart_popup.description": "Information about translation updates",
"Settings.translation_restart_popup.label": "Translation Update",
"Settings.translation_restart_popup.message1": "Some translations may not apply",
"Settings.translation_restart_popup.message2": "until you restart Blender.",
"TextureAtlas.atlas_completed": "Texture atlas creation completed",
"TextureAtlas.atlas_error": "An error occurred during texture atlas creation",
"TextureAtlas.atlas_materials": "Atlas Materials",
"TextureAtlas.atlas_materials_desc": "Atlas materials to optimize the model",
"TextureAtlas.label": "Texture Atlasing",
"TextureAtlas.loaded_list": "Loaded Texture Atlas Material List",
"TextureAtlas.material_list_label": "Texture Atlas Material List Material",
"TextureAtlas.reload_list": "Reload Texture Atlas Material List",
"TextureAtlas.error.label": "ERROR",
"TextureAtlas.none.label": "None",
"TextureAtlas.no_nodes_error.desc": "THIS MATERIAL DOES NOT USE NODES!",
"TextureAtlas.no_images_error.desc": "THIS MATERIAL HAS NO IMAGES!",
"TextureAtlas.texture_use_atlas.desc": "The texture that will be used for the {name} map atlas",
"TextureAtlas.albedo": "Albedo",
"TextureAtlas.normal": "Normal",
"TextureAtlas.emission": "Emission",
"TextureAtlas.ambient_occlusion": "Ambient Occlusion",
"TextureAtlas.height": "Height",
"TextureAtlas.roughness": "Roughness",
"Tools.bones_translated_success": "Successfully translated all bones to humanoid names",
"Tools.bones_translated_with_fails": "Failed to translate {translate_bone_fails} bones to humanoid names. Adding \"<noik>\" to their names.",
"Tools.convert_to_resonite.desc": "Converts bone names on a model to names compatible with Resonite",
"Tools.convert_to_resonite.label": "Convert to Resonite",
"Tools.create_digitigrade_legs.desc": "Create digitigrade legs from a selected bone chain",
"Tools.create_digitigrade_legs.label": "Create Digitigrade Legs",
"Tools.digitigrade_legs.error.bone_format": "Bone format incorrect! Please select a chain of 4 continuous bones!",
"Tools.digitigrade_legs.success": "Digitigrade legs created successfully",
"Tools.import_any_model.desc": "Import any supported model, FBX, SMD, DMX, GLTF, PMD, PMX and more.",
"Tools.import_any_model.label": "Import Model",
"UVTools.align_uv_to_target.warning.too_much": "Error! You have way to much stuff selected. Are you sure you're selecting two edges?",
"UVTools.align_uv_to_target.warning.need_a_line": "You need one line of selected uv points per selected object. Object \"{obj}\" does not meet this requirement!",
"avatar_toolkit.align_uv_edges_to_target.label":"Align UV Edges to Target",
"avatar_toolkit.align_uv_edges_to_target.desc":"Aligns a selected line of UV points on each selected mesh\nto the line of selected uv points on the active mesh.\nUseful for kitbashing textures of one model onto another.\nUses distance from the 2D cursor to identify the start of the line of uv points on each mesh.",
"Tools.label": "Tools",
"Tools.no_armature_selected": "No armature selected",
"Tools.select_armature": "Please select an armature",
"Tools.tools_title.label": "Tools:",
"Tools.separate_by.label": "Separate By:",
"Tools.separate_by_materials.label": "Separate by Materials",
"Tools.separate_by_materials.desc": "Separate the selected mesh by materials",
"Tools.separate_by_materials.success": "Mesh separated by materials successfully",
"Tools.separate_by_loose_parts.label": "Separate by Loose Parts",
"Tools.separate_by_loose_parts.desc": "Separate the selected mesh by loose parts",
"Tools.separate_by_loose_parts.success": "Mesh separated by loose parts successfully",
"Tools.apply_transforms.label": "Apply Transforms",
"Tools.apply_transforms.desc": "Apply position, rotation, and scale to the armature and its meshes",
"Tools.apply_transforms.invalid_armature": "Invalid armature selected",
"Tools.apply_transforms.success": "Transforms applied successfully to armature and meshes",
"Tools.remove_unused_shapekeys.label": "Remove Unused Shapekeys",
"Tools.remove_unused_shapekeys.tolerance.desc": "Min movement for position on any coordinate\n for any vertex for a shapekey to be kept.",
"Tools.remove_unused_shapekeys.desc": "Remove shapekeys that don't move anything.\nDoesn't get rid of category shapekeys.\n(ex: has \"~\", \"-\", or \"=\" in the name.)",
"Tools.remove_unused_shapekeys.tolerance.label": "Position Tolerance",
"Tools.apply_shape_key.label": "Apply Shapekey to Basis",
"Tools.apply_shape_key.desc": "Apply the selected shapekey to the basis, making it default on.",
"Tools.apply_shape_key.error": "The shape keys were not merged for some reason!",
"Tools.remove_zero_weight_bones.success": "Zero weight bones removed successfully",
"Tools.remove_zero_weight_bones.label": "Remove Zero Weight Bones",
"Tools.remove_zero_weight_bones.desc": "Remove bones from the armature that have weights less than threshold.",
"Tools.merge_bones_to_active.delete_old.desc": "Remove old bones when merging.",
"Tools.merge_bones_to_active.delete_old.label": "Remove Old Bones",
"Tools.merge_bones_to_active.desc": "Merge selected bones to active bone (selected in bright blue or orange).",
"Tools.merge_bones_to_active.label": "Merge Bones to Active",
"Tools.merge_bones_to_parents.delete_old.desc": "Remove old bones when merging.",
"Tools.merge_bones_to_parents.delete_old.label": "Remove Old Bones",
"Tools.merge_bones_to_parents.desc": "Merges every bone in the selection to each of their parents.",
"Tools.merge_bones_to_parents.label": "Merge Bones to Individual Parents",
"Tools.remove_zero_weight_bones.threshold.label": "Weight Threshold",
"Tools.remove_zero_weight_bones.threshold.desc": "If a bone is not weighted to any part of any mesh under the armature with a threshold greater than this, it is removed",
"Tools.connect_bones.label": "Connect Bones",
"Tools.bone_tools.label": "Bone Tools",
"Tools.additional_tools.label": "Additional Tools",
"Tools.merge_twist_bones.label": "Merge Twist Bones",
"Tools.merge_twist_bones.desc": "Merge twist bones into their parent bones",
"Tools.connect_bones.desc": "Connect bones with their respective children",
"Tools.connect_bones.invalid_armature": "Invalid armature selected",
"Tools.connect_bones.min_distance.label": "Minimum Distance",
"Tools.connect_bones.min_distance.desc": "Minimum distance between bones to connect them",
"Tools.connect_bones.success": "Connected {bones_connected} bones successfully",
"Tools.delete_bone_constraints.label": "Delete Bone Constraints",
"Tools.delete_bone_constraints.desc": "Remove all constraints from bones in the armature",
"Tools.delete_bone_constraints.invalid_armature": "Invalid armature selected",
"Tools.delete_bone_constraints.success": "Removed {constraints_removed} constraints from bones",
"Tools.convert_rigify_to_unity.label": "Convert Rigify to Unity",
"Tools.convert_rigify_to_unity.desc": "Prepare Rigify armature for use in Unity",
"Tools.convert_rigify_to_unity.success": "Rigify armature successfully converted for Unity",
"MergeArmatures.select_armature": "Please select an armature",
"MergeArmatures.title.label": "Merge Armatures:",
"MergeArmatures.label": "Merge Armatures",
"MergeArmatures.selected_armature.label": "Armature to Merge From",
"MergeArmatures.selected_armature.desc": "The armature that should be merged into the targeted armature for Avatar Toolkit.",
"MergeArmatures.target_armature.label": "Armature to Merge To",
"MergeArmatures.target_armature.desc": "The armature that should be the target for merging armatures.",
"MergeArmature.merge_armatures.label": "Merge Armatures Together",
"MergeArmature.merge_armatures.desc": "Merge {selected_armature_label} to the targeted armature for Avatar Toolkit.",
"MergeArmature.merge_armatures.align_bones.label": "Align Bones",
"MergeArmature.merge_armatures.align_bones.desc": "Align bones from source armature to target armature,\nstretching bones to match before merging.",
"MergeArmature.merge_armatures.apply_transforms.label": "Apply Transforms",
"MergeArmature.merge_armatures.apply_transforms.desc": "Apply transforms on armature and it's meshes before merging.",
"VisemePanel.create_visemes": "Create Visemes",
"VisemePanel.creating_viseme": "Creating viseme: {viseme_name}",
"VisemePanel.creating_viseme_detail": "Creating viseme: {viseme_name}",
"VisemePanel.creating_visemes": "Creating visemes...",
"VisemePanel.error.noArmature": "No armature selected",
"VisemePanel.error.noMesh": "No mesh selected",
"VisemePanel.error.noShapekeys": "Selected mesh has no shape keys",
"VisemePanel.error.selectMesh": "Select a mesh to create visemes",
"VisemePanel.info.selectMesh": "Select a mesh to create visemes",
"VisemePanel.label": "Visemes",
"VisemePanel.mixing_shape": "Mixing shape: {shape_name} with value: {value}",
"VisemePanel.mouth_a.desc": "The shapekey for the 'A' mouth shape",
"VisemePanel.mouth_a.label": "Mouth A",
"VisemePanel.mouth_ch.desc": "The shapekey for the 'CH' mouth shape",
"VisemePanel.mouth_ch.label": "Mouth CH",
"VisemePanel.mouth_o.desc": "The shapekey for the 'O' mouth shape",
"VisemePanel.mouth_o.label": "Mouth O",
"VisemePanel.removing_existing_viseme": "Removing existing viseme: {viseme_name}",
"VisemePanel.removing_existing_visemes": "Removing existing visemes...",
"VisemePanel.select_mesh": "Select Mesh",
"VisemePanel.selected_mesh.label": "Selected Mesh",
"VisemePanel.selected_mesh.desc": "The currently selected mesh for viseme operations",
"VisemePanel.selected_shapes": "Selected shapes: A={shape_a}, O={shape_o}, CH={shape_ch}",
"VisemePanel.shape_intensity": "Shape Intensity",
"VisemePanel.shape_intensity_desc": "The intensity of the viseme shapekeys",
"VisemePanel.sorting_shapekeys": "Sorting shape keys...",
"VisemePanel.start_viseme_creation": "Starting viseme creation...",
"VisemePanel.viseme_created_successfully": "Viseme {viseme_name} created successfully",
"VisemePanel.viseme_creation_completed": "Viseme creation completed.",
"MMDOptions.title": "MMD Options",
"MMDOptions.no_armature_selected": "No armature selected",
"MMDOptions.label": "MMD Options",
"MMDOptions.cleanup_mesh.label": "Cleanup Mesh",
"MMDOptions.cleanup_mesh.desc": "Clean up the mesh by removing empty objects, unused vertex groups, unused vertices, and empty shape keys",
"MMDOptions.removing_empty_objects": "Removing empty objects",
"MMDOptions.removing_unused_vertex_groups": "Removing unused vertex groups",
"MMDOptions.removing_unused_vertices": "Removing unused vertices",
"MMDOptions.removing_empty_shape_keys": "Removing empty shape keys",
"MMDOptions.optimize_weights.label": "Optimize Weights",
"MMDOptions.optimize_weights.desc": "Optimize vertex weights by limiting the number of weights per vertex",
"MMDOptions.max_weights.label": "Max Weights",
"MMDOptions.max_weights.desc": "Maximum number of weights per vertex",
"MMDOptions.merging_weights": "Merging weights",
"MMDOptions.removing_zero_weight_bones": "Removing zero weight bones",
"MMDOptions.limiting_vertex_weights": "Limiting vertex weights",
"MMDOptions.weight_optimization_complete": "Weight optimization complete",
"MMDOptions.optimize_armature.label": "Optimize Armature",
"MMDOptions.optimize_armature.desc": "Optimize the armature by fixing bone rolls, aligning bones, connecting bones, and more",
"MMDOptions.fixing_bone_rolls": "Fixing bone rolls",
"MMDOptions.aligning_bones": "Aligning bones",
"MMDOptions.connecting_bones": "Connecting bones",
"MMDOptions.deleting_bone_constraints": "Deleting bone constraints",
"MMDOptions.merging_bones_to_parents": "Merging bones to parents",
"MMDOptions.reordering_bones": "Reordering bones",
"MMDOptions.fixing_armature_names": "Fixing armature names",
"MMDOptions.renaming_bones": "Renaming bones",
"MMDOptions.armature_optimization_complete": "Armature optimization complete",
"MMDOptions.convert_materials.label": "Convert Materials",
"MMDOptions.convert_materials.desc": "Convert materials to use Principled BSDF shader and fix MMD and VRM shaders",
"MMDOptions.converting_materials": "Converting materials for {name}",
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.1.0)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.",
"Updater.label": "Updater",
"Updater.CheckForUpdateButton.label": "Check for Updates",
"Updater.CheckForUpdateButton.label_alt": "No Updates Available",
"Updater.UpdateToLatestButton.label": "Update to {name}",
"Updater.UpdateToSelectedButton.label": "Update",
"Updater.currentVersion": "Current Version: {name}",
"Updater.selectVersion": "Select Version",
"Updater.CheckForUpdateButton.desc": "Check for available updates",
"UpdateToLatestButton.desc": "Update to the latest version",
"UpdateNotificationPopup.label": "Update Notification",
@@ -276,25 +25,379 @@
"download_file.cantConnect": "Cannot connect to update server",
"download_file.cantFindZip": "Update file not found",
"download_file.cantFindAvatarToolkit": "Avatar Toolkit files not found in update package",
"CreditsSupport.label": "Credits & Support",
"CreditsSupport.credits_title": "Credits",
"CreditsSupport.credits_text1": "Avatar Toolkit has been created by the Neoneko team:",
"CreditsSupport.credits_text2": "Yusarina and 989Onan",
"CreditsSupport.credits_text3": "Some code has been inspired by Cats Blender Plugin,",
"CreditsSupport.credits_text4": "thanks to the original contributors to that plugin.",
"CreditsSupport.support_text1": "If you like what we do, you can donate/ tip to us",
"CreditsSupport.support_text2": "through our pally.gg page.",
"CreditsSupport.support_title": "Support Us",
"CreditsSupport.support_button": "Support Us",
"CreditsSupport.help_title": "Need Help?",
"CreditsSupport.help_text1": "Check out our wiki first, we HIGHLY encourage",
"CreditsSupport.help_text2": "that you read it before seeking further support.",
"CreditsSupport.wiki_button": "Wiki",
"CreditsSupport.discord_button": "Join Discord",
"TextureAtlas.include_in_atlas": "Include in Atlas",
"TextureAtlas.include_in_atlas_desc": "Include this material in the texture atlas",
"QuickAccess.label": "Quick Access",
"QuickAccess.select_armature": "Select Armature",
"QuickAccess.valid_armature": "Valid Armature",
"QuickAccess.bones_count": "Bones: {count}",
"QuickAccess.pose_bones_available": "Pose bones: Available",
"QuickAccess.pose_controls": "Pose Controls",
"QuickAccess.import_export": "Import/Export",
"QuickAccess.import": "Import",
"QuickAccess.export": "Export",
"QuickAccess.export_fbx": "Export FBX",
"QuickAccess.export_resonite": "Export to Resonite",
"QuickAccess.start_pose_mode.label": "Start Pose Mode",
"QuickAccess.start_pose_mode.desc": "Enter pose mode for the selected armature",
"QuickAccess.stop_pose_mode.label": "Stop Pose Mode",
"QuickAccess.stop_pose_mode.desc": "Exit pose mode and clear transforms",
"QuickAccess.apply_pose_as_shapekey.label": "Apply Pose as Shape Key",
"QuickAccess.apply_pose_as_shapekey.desc": "Create a new shape key from current pose",
"QuickAccess.apply_pose_as_rest.label": "Apply Pose as Rest",
"QuickAccess.apply_pose_as_rest.desc": "Apply current pose as rest pose",
"QuickAccess.apply_armature_failed": "Failed to apply armature modifications",
"QuickAccess.validation_basic_warning": "Limited Validation Active",
"QuickAccess.validation_basic_details": "Only essential bone structure is being validated",
"QuickAccess.validation_none_warning": "Validation Disabled",
"QuickAccess.validation_none_details": "No armature validation checks are being performed",
"PoseMode.error.start": "Failed to start pose mode: {error}",
"PoseMode.error.stop": "Failed to stop pose mode: {error}",
"PoseMode.error.shapekey": "Failed to apply pose as shape key: {error}",
"PoseMode.error.rest_pose": "Failed to apply pose as rest: {error}",
"PoseMode.shapekey.name": "Shape Key Name",
"PoseMode.shapekey.description": "Name for the new shape key",
"PoseMode.shapekey.default": "Pose_Shapekey",
"PoseMode.skipped_meshes": "Some meshes were skipped:\n{message}",
"PoseMode.basis": "Basis",
"Armature.validation.no_armature": "No armature selected",
"Armature.validation.not_armature": "Selected object is not an armature",
"Armature.validation.no_bones": "Armature has no bones",
"Armature.validation.basic_check_failed": "Basic armature validation failed",
"Armature.validation.missing_bones": "Missing essential bones: {bones}",
"Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}",
"Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}",
"Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists",
"Mesh.validation.no_data": "No mesh data",
"Mesh.validation.no_vertex_groups": "No vertex groups found",
"Mesh.validation.no_armature_modifier": "No armature modifier",
"Mesh.validation.valid": "Valid mesh for pose operations",
"Operation.pose_applied": "Pose applied successfully",
"Scene.avatar_toolkit_updater_version_list.name": "Version List",
"Scene.avatar_toolkit_updater_version_list.description": "List of available versions to update to",
"TextureAtlas.no_materials_selected": "No materials selected for atlas"
"Scene.avatar_toolkit_updater_version_list.description": "List of available versions",
"Optimization.label": "Optimization",
"Optimization.materials_title": "Materials",
"Optimization.cleanup_title": "Mesh Cleanup",
"Optimization.join_meshes_title": "Join Meshes",
"Optimization.combine_materials": "Combine Materials",
"Optimization.combine_materials_desc": "Combine similar materials to reduce draw calls",
"Optimization.remove_doubles": "Remove Doubles",
"Optimization.remove_doubles_desc": "Remove duplicate vertices",
"Optimization.remove_doubles_advanced": "Advanced",
"Optimization.remove_doubles_advanced_desc": "Remove duplicate vertices with advanced options",
"Optimization.join_all_meshes": "Join All",
"Optimization.join_all_meshes_desc": "Join all meshes in the scene",
"Optimization.join_selected_meshes": "Join Selected",
"Optimization.join_selected_meshes_desc": "Join only selected meshes",
"Optimization.no_meshes": "No meshes found to optimize",
"Optimization.materials_combined": "Combined {combined} materials, cleaned {cleaned} slots, and removed {removed} unused data blocks",
"Optimization.error.combine_materials": "Failed to combine materials: {error}",
"Optimization.materials_total": "Total Materials: {count}",
"Optimization.materials_duplicates": "Potential Duplicates: {count}",
"Optimization.no_materials": "No materials found on meshes",
"Optimization.error.consolidation": "Failed to consolidate materials. Check console for details",
"Optimization.combining_materials": "Combining similar materials...",
"Optimization.cleaning_slots": "Cleaning material slots...",
"Optimization.removing_unused": "Removing unused materials...",
"Optimization.selecting_meshes": "Selecting meshes...",
"Optimization.joining_meshes": "Joining meshes...",
"Optimization.applying_transforms": "Applying transforms...",
"Optimization.fixing_uvs": "Fixing UV coordinates...",
"Optimization.finalizing": "Finalizing...",
"Optimization.meshes_joined": "All meshes joined successfully",
"Optimization.selected_meshes_joined": "Selected meshes joined successfully",
"Optimization.no_mesh_selected": "No meshes selected",
"Optimization.select_at_least_two": "Please select at least two meshes",
"Optimization.error.join_meshes": "Failed to join meshes: {error}",
"Optimization.error.join_selected": "Failed to join selected meshes: {error}",
"Optimization.merge_distance": "Merge Distance",
"Optimization.merge_distance_desc": "Distance within which vertices will be merged",
"Optimization.remove_doubles_warning": "This process may take a long time",
"Optimization.remove_doubles_wait": "Blender may seem unresponsive during this operation",
"Optimization.error.remove_doubles": "Failed to remove doubles: {error}",
"Optimization.no_armature": "No armature selected",
"Optimization.processing_mesh": "Processing mesh: {name}",
"Optimization.processing_shapekey": "Processing shape key: {name}",
"Optimization.remove_doubles_completed": "Remove doubles completed successfully",
"Tools.label": "Tools",
"Tools.general_title": "General Tools",
"Tools.convert_resonite": "Convert to Resonite",
"Tools.convert_resonite_desc": "Convert model for use in Resonite",
"Tools.convert_resonite.operation": "Converting to Resonite",
"Tools.separate_title": "Separation Tools",
"Tools.separate_materials": "By Materials",
"Tools.separate_materials_desc": "Separate mesh by materials",
"Tools.separate_loose": "Loose Parts",
"Tools.separate_loose_desc": "Separate mesh into loose parts",
"Tools.separate_materials_success": "Mesh separated by materials successfully",
"Tools.separate_loose_success": "Mesh separated into loose parts successfully",
"Tools.bone_title": "Bone Tools",
"Tools.create_digitigrade": "Create Digitigrade Legs",
"Tools.create_digitigrade_desc": "Convert legs to digitigrade setup",
"Tools.digitigrade": "Create Digitigrade Legs",
"Tools.digitigrade_desc": "Convert selected leg bones to digitigrade setup",
"Tools.digitigrade_error": "Failed to create digitigrade legs: {error}",
"Tools.digitigrade_success": "Successfully created digitigrade leg setup",
"Tools.processing_leg": "Processing leg bone: {bone}",
"Tools.merge_twist_bones": "Keep Twist Bones",
"Tools.merge_twist_bones_desc": "When checked, twist bones will be kept, even if there are zero-weight",
"Tools.clean_weights": "Remove Zero Weight Bones",
"Tools.clean_weights_desc": "Remove bones with no vertex weights",
"Tools.clean_constraints": "Delete Bone Constraints",
"Tools.clean_constraints_desc": "Remove all bone constraints from armature",
"Tools.clean_constraints_success": "Removed {count} bone constraints",
"Tools.processing_bone_constraints": "Removing constraints from bone: {bone}",
"Tools.clean_weights_success": "Removed {count} zero-weight bones",
"Tools.clean_weights_threshold": "Weight Threshold",
"Tools.clean_weights_threshold_desc": "Minimum weight value to consider a bone as weighted",
"Tools.merge_title": "Merge Tools",
"Tools.merge_to_active": "Merge to Active",
"Tools.merge_to_active_desc": "Merge selected bones to active bone",
"Tools.merge_to_parent": "Merge to Parent",
"Tools.merge_to_parent_desc": "Merge bones to their respective parents",
"Tools.connect_bones": "Connect Bones",
"Tools.connect_bones_desc": "Connect disconnected bones in chain",
"Tools.additional_title": "Additional Tools",
"Tools.apply_transforms": "Apply Transforms",
"Tools.apply_transforms_desc": "Apply all transformations to objects",
"Tools.clean_shapekeys": "Remove Unused Shapekeys",
"Tools.clean_shapekeys_desc": "Remove unused shape keys from meshes",
"Tools.bones_translated_success": "All bones translated successfully",
"Tools.bones_translated_with_fails": "Translation completed with {translate_bone_fails} untranslated bones",
"Tools.storing_transforms": "Storing bone transforms...",
"Tools.analyzing_weights": "Analyzing vertex weights...",
"Tools.removing_bones": "Removing unweighted bones...",
"Tools.verifying_hierarchy": "Verifying bone hierarchy...",
"Tools.connect_bones_min_distance": "Minimum Distance",
"Tools.connect_bones_min_distance_desc": "Minimum distance between bones to attempt connection",
"Tools.connect_bones_success": "Connected {count} bones",
"Tools.merge_weights_threshold": "Weight Transfer Threshold",
"Tools.merge_weights_threshold_desc": "Minimum weight value to transfer when merging bones",
"Tools.no_bones_selected": "No bones selected to merge",
"Tools.no_bones_with_parent": "No selected bones with parents found",
"Tools.merge_to_active_success": "Successfully merged {count} bones to active bone",
"Tools.merge_to_parent_success": "Successfully merged {count} bones to their parents",
"Tools.transforms_applied": "Transforms applied successfully",
"Tools.shapekey_tolerance": "Shape Key Tolerance",
"Tools.shapekey_tolerance_desc": "Minimum difference to consider a shape key as used",
"Tools.shapekeys_removed": "Removed {count} unused shape keys",
"MMD.label": "MMD Tools",
"MMD.bone_standardization": "Bone Standardization",
"MMD.weight_processing": "Weight Processing",
"MMD.hierarchy": "Bone Hierarchy",
"MMD.cleanup": "Cleanup",
"MMD.no_armature": "No armature selected",
"MMD.no_meshes": "No meshes found",
"MMD.validation.rigify_unsupported": "Rigify armatures are not supported",
"MMD.validation.multi_user_mesh": "Multi-user mesh detected: {mesh}",
"MMD.bones_standardized": "Bones standardized successfully",
"MMD.weights_processed": "Weights processed successfully",
"MMD.hierarchy_fixed": "Bone hierarchy fixed successfully",
"MMD.hierarchy_validation_warning": "Some hierarchy relationships could not be validated",
"MMD.cleanup_completed": "Armature cleanup completed",
"MMD.process_twist_bones": "Process Twist Bones",
"MMD.process_twist_bones_desc": "Transfer weights from twist bones to their parent bones",
"MMD.connect_bones": "Connect Bones",
"MMD.connect_bones_desc": "Connect bones in chain where appropriate",
"Visemes.panel_label": "Visemes",
"Visemes.shape_selection": "Shape Key Selection",
"Visemes.controls": "Viseme Controls",
"Visemes.no_shapekeys": "Select a mesh with shape keys",
"Visemes.mouth_a": "A Shape",
"Visemes.mouth_a_desc": "Shape key for 'A' sound",
"Visemes.mouth_o": "O Shape",
"Visemes.mouth_o_desc": "Shape key for 'O' sound",
"Visemes.mouth_ch": "CH Shape",
"Visemes.mouth_ch_desc": "Shape key for 'CH' sound",
"Visemes.shape_intensity": "Shape Intensity",
"Visemes.shape_intensity_desc": "Intensity multiplier for viseme shapes",
"Visemes.start_preview": "Start Preview",
"Visemes.stop_preview": "Stop Preview",
"Visemes.preview_mode_desc": "Toggle viseme preview mode",
"Visemes.preview_selection": "Preview Selection",
"Visemes.preview_selection_desc": "Select viseme to preview",
"Visemes.preview_label": "Preview Visemes",
"Visemes.preview_desc": "Preview viseme shapes in viewport",
"Visemes.create_label": "Create Visemes",
"Visemes.create_desc": "Create VRC viseme shape keys",
"Visemes.error.no_shapekeys": "Mesh has no shape keys",
"Visemes.error.select_shapekeys": "Please select shape keys for A, O and CH",
"Visemes.success": "Visemes created successfully",
"EyeTracking.label": "Eye Tracking",
"EyeTracking.setup": "Eye Tracking Setup",
"EyeTracking.mesh_select": "Mesh Selection",
"EyeTracking.bones": "Bone Selection",
"EyeTracking.head_bone": "Head Bone",
"EyeTracking.eye_left": "Left Eye Bone",
"EyeTracking.eye_right": "Right Eye Bone",
"EyeTracking.shapekeys": "Shape Key Selection",
"EyeTracking.options": "Options",
"EyeTracking.rotation": "Eye Rotation",
"EyeTracking.rotation.x": "Vertical Rotation",
"EyeTracking.rotation.y": "Horizontal Rotation",
"EyeTracking.adjust": "Eye Adjustments",
"EyeTracking.blinking": "Blinking Controls",
"EyeTracking.no_shapekeys": "No shape keys found on selected mesh",
"EyeTracking.no_armature": "No armature selected",
"EyeTracking.no_mesh": "No mesh found",
"EyeTracking.create.label": "Create Eye Tracking",
"EyeTracking.create.desc": "Set up eye tracking bones and shape keys",
"EyeTracking.testing.start.label": "Start Testing",
"EyeTracking.testing.start.desc": "Enter eye tracking test mode",
"EyeTracking.testing.stop.label": "Stop Testing",
"EyeTracking.testing.stop.desc": "Exit eye tracking test mode",
"EyeTracking.reset.label": "Reset Eye Tracking",
"EyeTracking.reset.desc": "Reset all eye tracking settings",
"EyeTracking.rotate.label": "Rotate Eye Bones",
"EyeTracking.rotate.desc": "Rotate eye bones for VRChat compatibility",
"EyeTracking.iris.label": "Adjust Iris Height",
"EyeTracking.iris.desc": "Adjust the height of iris vertices",
"EyeTracking.blink.test.label": "Test Blink",
"EyeTracking.blink.test.desc": "Test eye blinking shape keys",
"EyeTracking.lowerlid.test.label": "Test Lower Lid",
"EyeTracking.lowerlid.test.desc": "Test lower lid shape keys",
"EyeTracking.blink.reset.label": "Reset Blink Test",
"EyeTracking.blink.reset.desc": "Reset blink testing values",
"EyeTracking.validation.noArmature": "No armature found in scene",
"EyeTracking.validation.noMesh": "Mesh '{mesh}' not found",
"EyeTracking.validation.noShapekeys": "Selected mesh has no shape keys",
"EyeTracking.validation.leftEye": "Left Eye",
"EyeTracking.validation.rightEye": "Right Eye",
"EyeTracking.validation.missingGroups": "Missing vertex groups: {groups}",
"EyeTracking.validation.missingBones": "Missing required bones: {bones}",
"EyeTracking.validation.success": "Eye tracking setup validated successfully",
"EyeTracking.error.noMesh": "No mesh selected for eye tracking",
"EyeTracking.error.noVertexGroup": "No vertex group found for bone: {bone}",
"EyeTracking.error.noShapeSelected": "Please select all required shape keys",
"EyeTracking.success": "Eye tracking setup completed successfully",
"EyeTracking.mode_select": "Mode Selection",
"EyeTracking.mesh_setup": "Mesh Setup",
"EyeTracking.bone_setup": "Bone Setup",
"EyeTracking.shapekey_setup": "Shape Key Setup",
"EyeTracking.testing": "Testing Mode",
"EyeTracking.rotation_controls": "Eye Rotation Controls",
"EyeTracking.adjustments": "Eye Adjustments",
"EyeTracking.blink_testing": "Blink Testing",
"EyeTracking.wink_left": "Left Wink",
"EyeTracking.wink_right": "Right Wink",
"EyeTracking.lowerlid_left": "Left Lower Lid",
"EyeTracking.lowerlid_right": "Right Lower Lid",
"EyeTracking.mode.creation": "Creation Mode",
"EyeTracking.mode.testing": "Testing Mode",
"EyeTracking.disable_blinking": "Disable Eye Blinking",
"EyeTracking.disable_movement": "Disable Eye Movement",
"EyeTracking.distance": "Eye Distance",
"EyeTracking.distance_desc": "Adjust the distance between eyes",
"EyeTracking.mode": "Eye Tracking Mode",
"EyeTracking.mesh_name": "Mesh",
"EyeTracking.mesh_name_desc": "Select mesh for eye tracking",
"EyeTracking.head_bone_desc": "Select head bone",
"EyeTracking.eye_left_desc": "Select left eye bone",
"EyeTracking.eye_right_desc": "Select right eye bone",
"EyeTracking.type": "Eye Tracking Type",
"EyeTracking.type_desc": "Select the type of eye tracking setup to create",
"EyeTracking.create.av3.label": "Create AV3 Eye Tracking",
"EyeTracking.create.av3.desc": "Set up eye tracking for VRChat Avatar 3.0",
"EyeTracking.create.sdk2.label": "Create SDK2 Eye Tracking",
"EyeTracking.create.sdk2.desc": "Set up eye tracking for VRChat SDK2",
"EyeTracking.sdk_version": "SDK Version",
"EyeTracking.type.av3": "Avatar 3.0",
"EyeTracking.type.av3_desc": "VRChat Avatar 3.0 eye tracking setup",
"EyeTracking.type.sdk2": "SDK2 (Legacy)",
"EyeTracking.type.sdk2_desc": "VRChat SDK2 eye tracking setup",
"CustomPanel.label": "Custom Avatar Tools",
"CustomPanel.merge_mode": "Merge Mode",
"CustomPanel.mesh_selection": "Mesh Selection",
"CustomPanel.select_mesh": "Select Mesh",
"CustomPanel.select_bone": "Select Bone",
"CustomPanel.select_armature": "Select Armature",
"CustomPanel.mode.armature": "Armature",
"CustomPanel.mode.armature_desc": "Merge armatures together",
"CustomPanel.mode.mesh": "Mesh",
"CustomPanel.mode.mesh_desc": "Attach meshes to armature",
"AttachMesh.label": "Attach Mesh",
"AttachMesh.desc": "Attach a mesh to an armature bone with automatic weight setup",
"AttachMesh.search_desc": "Search for meshes to attach",
"AttachMesh.select": "Select Mesh to Attach",
"AttachMesh.select_desc": "Choose a mesh to attach to the armature",
"AttachMesh.success": "Mesh attached successfully",
"AttachMesh.warn_no_armature": "Select an armature and mesh to attach",
"AttachMesh.validate_transforms": "Validating mesh transforms",
"AttachMesh.validate_name": "Validating mesh name",
"AttachMesh.parent_mesh": "Parenting mesh to armature",
"AttachMesh.setup_weights": "Setting up vertex weights",
"AttachMesh.create_bone": "Creating attachment bone",
"AttachMesh.position_bone": "Positioning bone",
"AttachMesh.add_modifier": "Adding armature modifier",
"AttachMesh.error.bone_not_found": "Attach bone '{bone}' not found",
"AttachMesh.error.mesh_not_found": "Mesh not found",
"AttachMesh.error.non_uniform_scale": "Mesh has non-uniform scale. Please apply scale",
"AttachBone.search_desc": "Search for target bone",
"AttachBone.select": "Select Target Bone",
"AttachBone.select_desc": "Choose the bone to attach the mesh to",
"MergeArmature.label": "Merge Armatures",
"MergeArmature.desc": "Merge two armatures together",
"MergeArmature.options": "Merge Options",
"MergeArmature.warn_two": "Need at least two armatures to merge",
"MergeArmature.into": "Merge Into",
"MergeArmature.into_desc": "Target armature to merge into",
"MergeArmature.into_search_desc": "Search for target armature",
"MergeArmature.from": "Merge From",
"MergeArmature.from_desc": "Source armature to merge from",
"MergeArmature.from_search_desc": "Search for source armature",
"MergeArmature.error.not_found": "Armature '{name}' not found",
"MergeArmature.error.transforms_not_aligned": "Transforms must be applied to merge this armature, either do this via the manual method or via apply transform checkmark",
"MergeArmature.error.check_transforms": "Please check parent transformations",
"MergeArmature.error.fix_parents": "Please fix parent relationships",
"MergeArmature.progress.removing_rigidbodies": "Removing rigid bodies and joints",
"MergeArmature.progress.validating": "Validating armatures",
"MergeArmature.progress.merging": "Merging armatures",
"MergeArmature.success": "Armatures merged successfully",
"MergeArmature.merge_all": "Merge Same Bones",
"MergeArmature.merge_all_desc": "Merge bones with matching names",
"MergeArmature.apply_transforms": "Apply Transforms",
"MergeArmature.apply_transforms_desc": "Apply all transformations before merging",
"MergeArmature.join_meshes": "Join Meshes",
"MergeArmature.join_meshes_desc": "Join meshes after merging",
"MergeArmature.remove_zero_weights": "Remove Zero Weights",
"MergeArmature.remove_zero_weights_desc": "Remove vertex groups with no weights",
"MergeArmature.cleanup_shape_keys": "Clean Shape Keys",
"MergeArmature.cleanup_shape_keys_desc": "Remove unused shape keys",
"Settings.label": "Settings",
"Settings.language": "Language",
"Settings.language_desc": "Select interface language",
"Settings.validation_mode": "Validation Mode",
"Settings.validation_mode_desc": "Choose how strictly to validate armatures",
"Settings.validation_mode.strict": "Strict",
"Settings.validation_mode.strict_desc": "Full validation including bone hierarchy and symmetry",
"Settings.validation_mode.basic": "Basic",
"Settings.validation_mode.basic_desc": "Essential bones check only",
"Settings.validation_mode.none": "None",
"Settings.validation_mode.none_desc": "No armature validation",
"Settings.debug": "Debug Settings",
"Settings.logging": "Logging",
"Settings.enable_logging": "Enable Debug Logging",
"Settings.enable_logging_desc": "Enable detailed debug logging for troubleshooting",
"Settings.logging_enabled": "Debug logging enabled",
"Settings.logging_disabled": "Debug logging disabled",
"Language.auto": "Automatic",
"Language.en_US": "English",
"Language.ja_JP": "Japanese",
"Language.changed.title": "Language Changed",
"Language.changed.success": "Language changed successfully!",
"Language.changed.restart": "Some UI elements may require restarting Blender"
}
}
-15
View File
@@ -1,15 +0,0 @@
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')]:
print("reloading " +module_name)
exec("importlib.reload("+module_name+")")
-197
View File
@@ -1,197 +0,0 @@
from bpy.types import UIList, Panel, UILayout, Object, Context,Material, Operator
import bpy
from math import sqrt
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.common import SceneMatClass, MaterialListBool, get_selected_armature
from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials
from ..functions.translations import t
@register_wrap
class AvatarToolKit_OT_SelectAllMaterials(Operator):
bl_idname = 'avatar_toolkit.select_all_materials'
bl_label = "Select All"
bl_description = "Select all materials for atlas"
def execute(self, context):
for item in context.scene.materials:
item.mat.include_in_atlas = True
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_SelectNoneMaterials(Operator):
bl_idname = 'avatar_toolkit.select_none_materials'
bl_label = "Select None"
bl_description = "Deselect all materials"
def execute(self, context):
for item in context.scene.materials:
item.mat.include_in_atlas = False
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_ExpandAllMaterials(Operator):
bl_idname = 'avatar_toolkit.expand_all_materials'
bl_label = "Expand All"
bl_description = "Expand all material settings"
def execute(self, context):
for item in context.scene.materials:
item.mat.material_expanded = True
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_CollapseAllMaterials(Operator):
bl_idname = 'avatar_toolkit.collapse_all_materials'
bl_label = "Collapse All"
bl_description = "Collapse all material settings"
def execute(self, context):
for item in context.scene.materials:
item.mat.material_expanded = False
return {'FINISHED'}
@register_wrap
class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
bl_idname = 'avatar_toolkit.expand_section_materials'
bl_label = ""
bl_description = ""
@classmethod
def poll(cls, context: Context) -> bool:
return True
def execute(self, context: Context) -> set:
if not context.scene.texture_atlas_Has_Mat_List_Shown:
context.scene.materials.clear()
newlist: list[Material] = []
for obj in bpy.context.scene.objects:
if len(obj.material_slots)>0:
for mat_slot in obj.material_slots:
if mat_slot.material:
if mat_slot.material not in newlist:
newlist.append(mat_slot.material)
newitem: SceneMatClass = context.scene.materials.add()
newitem.mat = mat_slot.material
MaterialListBool.old_list[context.scene.name] = newlist
else:
context.scene.texture_atlas_Has_Mat_List_Shown = False
return {'FINISHED'}
@register_wrap
class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList):
bl_label = t("TextureAtlas.material_list_label")
bl_idname = "Material_UL_avatar_toolkit_texture_atlas_mat_list_mat"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
def draw_header(self, context):
layout = self.layout
row = layout.row(align=True)
row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT')
row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT')
row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN')
row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT')
row.prop(context.scene, "material_search_filter", text="", icon='VIEWZOOM')
box = layout.box()
row = box.row()
row.label(text=f"Estimated Atlas Size: {self.calculate_atlas_size(context)}px")
def draw_item(self, context: Context, layout: UILayout, data: Object, item: SceneMatClass, icon, active_data, active_propname, index):
if context.scene.texture_atlas_Has_Mat_List_Shown:
if context.scene.material_search_filter and context.scene.material_search_filter.lower() not in item.mat.name.lower():
return
row = layout.row()
# Add a clear checkbox for material selection
row.prop(item.mat, "include_in_atlas", text="", icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT')
# Material name and expansion toggle
row.prop(item.mat, "material_expanded",
text=item.mat.name,
icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW',
emboss=False)
# Show texture settings if expanded
if item.mat.material_expanded and item.mat.include_in_atlas:
box = layout.box()
col = box.column(align=True)
self.draw_texture_row(col, item.mat, "texture_atlas_albedo", "IMAGE_RGB")
self.draw_texture_row(col, item.mat, "texture_atlas_normal", "NORMALS_FACE")
self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT")
self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID")
self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH")
self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL")
col.separator(factor=0.5)
def draw_texture_row(self, layout, material, prop_name, icon):
row = layout.row()
row.prop(material, prop_name, icon=icon)
if getattr(material, prop_name):
row.label(text="", icon='CHECKMARK')
else:
row.label(text="", icon='X')
def is_material_ready(self, material):
return bool(material.texture_atlas_albedo or
material.texture_atlas_normal or
material.texture_atlas_emission)
def calculate_atlas_size(self, context):
total_size = 0
for mat in context.scene.materials:
if mat.mat.include_in_atlas:
if mat.mat.texture_atlas_albedo:
img = bpy.data.images[mat.mat.texture_atlas_albedo]
total_size += img.size[0] * img.size[1]
return f"{int(sqrt(total_size))}x{int(sqrt(total_size))}"
@register_wrap
class AvatarToolKit_PT_TextureAtlasPanel(Panel):
bl_label = t("TextureAtlas.label")
bl_idname = "OBJECT_PT_avatar_toolkit_texture_atlas"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 6
def draw(self, context: Context):
layout = self.layout
armature = get_selected_armature(context)
if armature:
layout.label(text=t("TextureAtlas.label"), icon='TEXTURE')
layout.separator(factor=0.5)
box = layout.box()
row = box.row()
direction_icon = 'RIGHTARROW' if not context.scene.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT'
row.operator(AvatarToolKit_OT_ExpandSectionMaterials.bl_idname,
text=(t("TextureAtlas.reload_list") if not context.scene.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list")),
icon=direction_icon)
if context.scene.texture_atlas_Has_Mat_List_Shown:
row = box.row()
row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname,
'material_list',
context.scene,
'materials',
context.scene,
'texture_atlas_material_index',
rows=12,
type='DEFAULT')
layout.separator(factor=1.0)
row = layout.row()
row.scale_y = 1.5
row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname,
text=t("TextureAtlas.atlas_materials"),
icon='NODE_TEXTURE')
else:
layout.label(text=t("Tools.select_armature"), icon='ERROR')
-55
View File
@@ -1,55 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..functions.translations import t
from ..core.common import open_web_after_delay_multi_threaded
@register_wrap
class AvatarToolkit_PT_CreditsSupport(bpy.types.Panel):
bl_label = t("CreditsSupport.label")
bl_idname = "OBJECT_PT_avatar_toolkit_credits_support"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 10
def draw(self, context):
layout = self.layout
layout.label(text=t("CreditsSupport.credits_title"))
box = layout.box()
column = box.column(align=True)
column.scale_y = 0.7
column.label(text=t("CreditsSupport.credits_text1"))
column.label(text=t("CreditsSupport.credits_text2"))
column.label(text=t("CreditsSupport.credits_text3"))
column.label(text=t("CreditsSupport.credits_text4"))
layout.separator()
layout.label(text=t("CreditsSupport.support_title"))
box = layout.box()
column = box.column(align=True)
column.scale_y = 0.7
column.label(text=t("CreditsSupport.support_text1"))
column.label(text=t("CreditsSupport.support_text2"))
row = column.row()
row.scale_y = 1.5
row.operator("wm.url_open", text=t("CreditsSupport.support_button")).url = "https://neoneko.xyz/supportus.html"
layout.separator()
layout.label(text=t("CreditsSupport.help_title"))
box = layout.box()
column = box.column(align=True)
column.scale_y = 0.7
column.label(text=t("CreditsSupport.help_text1"))
column.label(text=t("CreditsSupport.help_text2"))
row = column.row()
row.scale_y = 1.5
row.operator("wm.url_open", text=t("CreditsSupport.wiki_button")).url = "https://github.com/teamneoneko/Avatar-Toolkit"
row = column.row()
row.scale_y = 1.5
row.operator("wm.url_open", text=t("CreditsSupport.discord_button")).url = "https://discord.catsblenderplugin.xyz"
+227
View File
@@ -0,0 +1,227 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
from ..core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
get_armature_list
)
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
bl_idname = "avatar_toolkit.search_merge_armature_into"
bl_label = ""
bl_description = t('MergeArmature.into_search_desc')
bl_property = "search_merge_armature_into_enum"
search_merge_armature_into_enum: bpy.props.EnumProperty(
name=t('MergeArmature.into'),
description=t('MergeArmature.into_desc'),
items=get_armature_list
)
def execute(self, context):
context.scene.avatar_toolkit.merge_armature_into = self.search_merge_armature_into_enum
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolkit_OT_SearchMergeArmature(Operator):
bl_idname = "avatar_toolkit.search_merge_armature"
bl_label = ""
bl_description = t('MergeArmature.from_search_desc')
bl_property = "search_merge_armature_enum"
search_merge_armature_enum: bpy.props.EnumProperty(
name=t('MergeArmature.from'),
description=t('MergeArmature.from_desc'),
items=get_armature_list
)
def execute(self, context):
context.scene.avatar_toolkit.merge_armature = self.search_merge_armature_enum
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolkit_OT_SearchAttachMesh(Operator):
bl_idname = "avatar_toolkit.search_attach_mesh"
bl_label = ""
bl_description = t('AttachMesh.search_desc')
bl_property = "search_attach_mesh_enum"
search_attach_mesh_enum: bpy.props.EnumProperty(
name=t('AttachMesh.select'),
description=t('AttachMesh.select_desc'),
items=lambda self, context: [
(obj.name, obj.name, "")
for obj in bpy.data.objects
if obj.type == 'MESH'
and not any(mod.type == 'ARMATURE' for mod in obj.modifiers)
]
)
def execute(self, context):
context.scene.avatar_toolkit.attach_mesh = self.search_attach_mesh_enum
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolkit_OT_SearchAttachBone(Operator):
bl_idname = "avatar_toolkit.search_attach_bone"
bl_label = ""
bl_description = t('AttachBone.search_desc')
bl_property = "search_attach_bone_enum"
search_attach_bone_enum: bpy.props.EnumProperty(
name=t('AttachBone.select'),
description=t('AttachBone.select_desc'),
items=lambda self, context: [
(bone.name, bone.name, "")
for bone in get_active_armature(context).data.bones
] if get_active_armature(context) else []
)
def execute(self, context):
context.scene.avatar_toolkit.attach_bone = self.search_attach_bone_enum
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {'FINISHED'}
class AvatarToolKit_PT_CustomPanel(Panel):
"""Panel containing tools for custom avatar creation and merging"""
bl_label = t('CustomPanel.label')
bl_idname = "VIEW3D_PT_avatar_toolkit_custom"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 4
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
toolkit = context.scene.avatar_toolkit
# Mode Selection Box
mode_box: UILayout = layout.box()
col: UILayout = mode_box.column(align=True)
col.label(text=t('CustomPanel.merge_mode'), icon='TOOL_SETTINGS')
col.separator(factor=0.5)
row: UILayout = col.row(align=True)
row.scale_y = 1.5
row.prop(toolkit, "merge_mode", expand=True)
if toolkit.merge_mode == 'ARMATURE':
self.draw_armature_tools(layout, context)
else:
self.draw_mesh_tools(layout, context)
def draw_armature_tools(self, layout: UILayout, context: Context) -> None:
toolkit = context.scene.avatar_toolkit
# Merge Settings Box
settings_box: UILayout = layout.box()
col: UILayout = settings_box.column(align=True)
col.label(text=t('MergeArmature.label'), icon='ARMATURE_DATA')
col.separator(factor=0.5)
if len(get_armature_list(context)) <= 1:
col.label(text=t('MergeArmature.warn_two'), icon='INFO')
return
# Options Box with better spacing
options_box: UILayout = layout.box()
col: UILayout = options_box.column(align=True)
col.label(text=t('MergeArmature.options'), icon='SETTINGS')
col.separator(factor=0.5)
# Group related options together
transform_col = col.column(align=True)
transform_col.prop(toolkit, "merge_all_bones")
transform_col.prop(toolkit, "apply_transforms")
col.separator(factor=0.5)
cleanup_col = col.column(align=True)
cleanup_col.prop(toolkit, "join_meshes")
cleanup_col.prop(toolkit, "remove_zero_weights")
cleanup_col.prop(toolkit, "cleanup_shape_keys")
# Selection Box with consistent styling
selection_box: UILayout = layout.box()
col: UILayout = selection_box.column(align=True)
col.label(text=t('CustomPanel.select_armature'), icon='BONE_DATA')
col.separator(factor=0.5)
# Armature selection with better alignment
row: UILayout = col.row(align=True)
row.label(text=t('MergeArmature.into'), icon='ARMATURE_DATA')
row.operator("avatar_toolkit.search_merge_armature_into",
text=toolkit.merge_armature_into)
row: UILayout = col.row(align=True)
row.label(text=t('MergeArmature.from'), icon='ARMATURE_DATA')
row.operator("avatar_toolkit.search_merge_armature",
text=toolkit.merge_armature)
# Merge button with emphasis
merge_box: UILayout = layout.box()
col = merge_box.column(align=True)
row = col.row(align=True)
row.scale_y = 1.5
row.operator("avatar_toolkit.merge_armatures", icon='ARMATURE_DATA')
def draw_mesh_tools(self, layout: UILayout, context: Context) -> None:
toolkit = context.scene.avatar_toolkit
# Mesh Tools Box
tools_box: UILayout = layout.box()
col: UILayout = tools_box.column(align=True)
col.label(text=t('AttachMesh.label'), icon='MESH_DATA')
col.separator(factor=0.5)
if not get_active_armature(context) or not get_all_meshes(context):
col.label(text=t('AttachMesh.warn_no_armature'), icon='INFO')
return
# Selection Box with consistent styling
selection_box: UILayout = layout.box()
col: UILayout = selection_box.column(align=True)
col.label(text=t('CustomPanel.mesh_selection'), icon='OBJECT_DATA')
col.separator(factor=0.5)
# Selection rows with icons and better alignment
row: UILayout = col.row(align=True)
row.label(text=t('CustomPanel.select_armature'), icon='ARMATURE_DATA')
row.operator("avatar_toolkit.search_merge_armature_into",
text=toolkit.merge_armature_into)
row: UILayout = col.row(align=True)
row.label(text=t('CustomPanel.select_mesh'), icon='MESH_DATA')
row.operator("avatar_toolkit.search_attach_mesh",
text=toolkit.attach_mesh)
row: UILayout = col.row(align=True)
row.label(text=t('CustomPanel.select_bone'), icon='BONE_DATA')
row.operator("avatar_toolkit.search_attach_bone",
text=toolkit.attach_bone)
# Attach button with emphasis
attach_box: UILayout = layout.box()
col = attach_box.column(align=True)
row = col.row(align=True)
row.scale_y = 1.5
row.operator("avatar_toolkit.attach_mesh", icon='ARMATURE_DATA')
+187
View File
@@ -0,0 +1,187 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
from ..core.common import get_active_armature, get_all_meshes
from ..functions.eye_tracking import (
CreateEyesAV3Button,
CreateEyesSDK2Button,
StartTestingButton,
StopTestingButton,
ResetRotationButton,
AdjustEyesButton,
TestBlinking,
TestLowerlid,
ResetBlinkTest,
ResetEyeTrackingButton,
RotateEyeBonesForAv3Button
)
class AvatarToolKit_PT_EyeTrackingPanel(Panel):
"""Panel containing eye tracking setup and testing tools"""
bl_label = t("EyeTracking.label")
bl_idname = "VIEW3D_PT_avatar_toolkit_eye_tracking"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 6
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the eye tracking panel interface"""
layout = self.layout
toolkit = context.scene.avatar_toolkit
# SDK Version Selection Box
sdk_box = layout.box()
col = sdk_box.column(align=True)
col.label(text=t("EyeTracking.sdk_version"), icon='PRESET')
col.separator(factor=0.5)
row = col.row(align=True)
row.prop(toolkit, "eye_tracking_type", expand=True)
if toolkit.eye_tracking_type == 'SDK2':
# Mode Selection Box
mode_box = layout.box()
col = mode_box.column(align=True)
col.label(text=t("EyeTracking.setup"), icon='TOOL_SETTINGS')
col.separator(factor=0.5)
col.prop(toolkit, "eye_mode", expand=True)
if toolkit.eye_mode == 'CREATION':
self.draw_creation_mode(context, layout)
else:
self.draw_testing_mode(context, layout)
else:
# AV3 bone setup only
self.draw_av3_setup(context, layout)
def draw_av3_setup(self, context: Context, layout: UILayout) -> None:
toolkit = context.scene.avatar_toolkit
# Bone Setup Box
bone_box = layout.box()
col = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5)
armature = get_active_armature(context)
if armature:
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
col.prop_search(toolkit, "eye_left", armature.data, "bones", text=t("EyeTracking.eye_left"))
col.prop_search(toolkit, "eye_right", armature.data, "bones", text=t("EyeTracking.eye_right"))
else:
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Create Button
row = layout.row(align=True)
row.scale_y = 1.5
row.operator(CreateEyesAV3Button.bl_idname, icon='PLAY')
def draw_creation_mode(self, context: Context, layout: UILayout) -> None:
toolkit = context.scene.avatar_toolkit
# Bone Setup Box
bone_box = layout.box()
col = bone_box.column(align=True)
col.label(text=t("EyeTracking.bone_setup"), icon='BONE_DATA')
col.separator(factor=0.5)
armature = get_active_armature(context)
if armature:
col.prop_search(toolkit, "head", armature.data, "bones", text=t("EyeTracking.head_bone"))
col.prop_search(toolkit, "eye_left", armature.data, "bones", text=t("EyeTracking.eye_left"))
col.prop_search(toolkit, "eye_right", armature.data, "bones", text=t("EyeTracking.eye_right"))
else:
col.label(text=t("EyeTracking.no_armature"), icon='ERROR')
# Mesh Setup Box
mesh_box = layout.box()
col = mesh_box.column(align=True)
col.label(text=t("EyeTracking.mesh_setup"), icon='MESH_DATA')
col.separator(factor=0.5)
col.prop_search(toolkit, "mesh_name_eye", bpy.data, "objects", text="")
# Shape Key Setup Box
shape_box = layout.box()
col = shape_box.column(align=True)
col.label(text=t("EyeTracking.shapekey_setup"), icon='SHAPEKEY_DATA')
col.separator(factor=0.5)
mesh = bpy.data.objects.get(toolkit.mesh_name_eye)
if mesh and mesh.data.shape_keys:
col.prop_search(toolkit, "wink_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_left"))
col.prop_search(toolkit, "wink_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.wink_right"))
col.prop_search(toolkit, "lowerlid_left", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_left"))
col.prop_search(toolkit, "lowerlid_right", mesh.data.shape_keys, "key_blocks", text=t("EyeTracking.lowerlid_right"))
else:
col.label(text=t("EyeTracking.no_shapekeys"), icon='ERROR')
# Options Box
options_box = layout.box()
col = options_box.column(align=True)
col.label(text=t("EyeTracking.options"), icon='SETTINGS')
col.separator(factor=0.5)
col.prop(toolkit, "disable_eye_blinking")
col.prop(toolkit, "disable_eye_movement")
if not toolkit.disable_eye_movement:
col.prop(toolkit, "eye_distance")
# Create Button
row = layout.row(align=True)
row.scale_y = 1.5
row.operator(CreateEyesSDK2Button.bl_idname, icon='PLAY')
def draw_testing_mode(self, context: Context, layout: UILayout) -> None:
toolkit = context.scene.avatar_toolkit
if context.mode != 'POSE':
# Testing Start Box
test_box = layout.box()
col = test_box.column(align=True)
col.label(text=t("EyeTracking.testing"), icon='PLAY')
col.separator(factor=0.5)
row = col.row(align=True)
row.scale_y = 1.5
row.operator(StartTestingButton.bl_idname, icon='PLAY')
else:
# Eye Rotation Box
rotation_box = layout.box()
col = rotation_box.column(align=True)
col.label(text=t("EyeTracking.rotation_controls"), icon='DRIVER_ROTATIONAL_DIFFERENCE')
col.separator(factor=0.5)
col.prop(toolkit, "eye_rotation_x", text=t("EyeTracking.rotation.x"))
col.prop(toolkit, "eye_rotation_y", text=t("EyeTracking.rotation.y"))
col.operator(ResetRotationButton.bl_idname, icon='LOOP_BACK')
# Eye Adjustment Box
adjust_box = layout.box()
col = adjust_box.column(align=True)
col.label(text=t("EyeTracking.adjustments"), icon='MODIFIER')
col.separator(factor=0.5)
col.prop(toolkit, "eye_distance")
col.operator(AdjustEyesButton.bl_idname, icon='CON_TRACKTO')
# Blinking Test Box
blink_box = layout.box()
col = blink_box.column(align=True)
col.label(text=t("EyeTracking.blink_testing"), icon='HIDE_OFF')
col.separator(factor=0.5)
row = col.row(align=True)
row.prop(toolkit, "eye_blink_shape")
row.operator(TestBlinking.bl_idname, icon='RESTRICT_VIEW_OFF')
row = col.row(align=True)
row.prop(toolkit, "eye_lowerlid_shape")
row.operator(TestLowerlid.bl_idname, icon='RESTRICT_VIEW_OFF')
col.operator(ResetBlinkTest.bl_idname, icon='LOOP_BACK')
# Stop Testing Button
row = layout.row(align=True)
row.scale_y = 1.5
row.operator(StopTestingButton.bl_idname, icon='PAUSE')
# Reset Button
row = layout.row(align=True)
row.operator(ResetEyeTrackingButton.bl_idname, icon='FILE_REFRESH')
+37
View File
@@ -0,0 +1,37 @@
import bpy
from typing import Optional, Set
from bpy.types import Panel, Context, UILayout
from ..core.translations import t
CATEGORY_NAME: str = "Avatar Toolkit"
def draw_title(self: Panel) -> None:
"""Draw the main panel title and description"""
layout: UILayout = self.layout
box: UILayout = layout.box()
col: UILayout = box.column(align=True)
# Add a nice header
row: UILayout = col.row()
row.scale_y: float = 1.2
row.label(text=t("AvatarToolkit.label"), icon='ARMATURE_DATA')
# Description as a flowing paragraph
desc_col: UILayout = col.column()
desc_col.scale_y: float = 0.6
desc_col.label(text=t("AvatarToolkit.desc1"))
desc_col.label(text=t("AvatarToolkit.desc2"))
desc_col.label(text=t("AvatarToolkit.desc3"))
col.separator()
class AvatarToolKit_PT_AvatarToolkitPanel(Panel):
"""Main panel for Avatar Toolkit containing general information and settings"""
bl_label: str = t("AvatarToolkit.label")
bl_idname: str = "OBJECT_PT_avatar_toolkit"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
def draw(self, context: Context) -> None:
"""Draw the main panel layout"""
draw_title(self)
-48
View File
@@ -1,48 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from bpy.types import Panel, Context
from ..core.common import get_selected_armature
from ..functions.translations import t
from ..functions.armature_modifying import AvatarToolkit_OT_MergeArmatures
@register_wrap
class AvatarToolkit_PT_MergeArmaturesPanel(Panel):
bl_label = t("MergeArmatures.label")
bl_idname = "OBJECT_PT_avatar_toolkit_merge_armatures"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 5
def draw(self, context: Context):
layout = self.layout
armature = get_selected_armature(context)
if armature:
layout.label(text=t("MergeArmatures.title.label"), icon='ARMATURE_DATA')
layout.separator(factor=0.5)
box = layout.box()
col = box.column(align=True)
col.prop(context.scene, property="selected_armature", text=t("MergeArmatures.target_armature.label"), icon="ARMATURE_DATA")
col.prop(context.scene, property="merge_armature_source", icon="OUTLINER_OB_ARMATURE")
layout.separator(factor=0.5)
col = layout.column(align=True)
col.prop(context.scene, property="merge_armature_align_bones", icon="BONE_DATA")
col.prop(context.scene, property="merge_armature_apply_transforms", icon="OBJECT_ORIGIN")
layout.separator(factor=1.0)
row = layout.row()
row.scale_y = 1.5
row.operator(operator=AvatarToolkit_OT_MergeArmatures.bl_idname, icon="ARMATURE_DATA")
else:
layout.label(text=t("MergeArmatures.select_armature"), icon='ERROR')
-51
View File
@@ -1,51 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..functions.translations import t
from ..functions.mmd_functions import *
from ..functions.mesh_tools import AvatarToolKit_OT_JoinAllMeshes
from ..functions.combine_materials import AvatarToolKit_OT_CombineMaterials
from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms
@register_wrap
class AvatarToolkit_PT_MMDOptionsPanel(bpy.types.Panel):
bl_label = t("MMDOptions.label")
bl_idname = "OBJECT_PT_avatar_toolkit_mmd_options"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 4
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
layout.label(text=t("MMDOptions.title"), icon='OUTLINER_OB_ARMATURE')
layout.separator(factor=0.5)
col = layout.column(align=True)
col.scale_y = 1.2
col.operator(AvatarToolKit_OT_CleanupMesh.bl_idname, icon='BRUSH_DATA')
col.operator(AvatarToolKit_OT_JoinAllMeshes.bl_idname, icon='OBJECT_DATAMODE')
layout.separator(factor=0.5)
col = layout.column(align=True)
col.scale_y = 1.2
col.operator(AvatarToolKit_OT_OptimizeWeights.bl_idname, icon='MOD_VERTEX_WEIGHT')
col.operator(AvatarToolKit_OT_OptimizeArmature.bl_idname, icon='ARMATURE_DATA')
layout.separator(factor=0.5)
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(AvatarToolKit_OT_ApplyTransforms.bl_idname, icon='OBJECT_ORIGIN')
row.operator(AvatarToolKit_OT_CombineMaterials.bl_idname, icon='MATERIAL')
layout.separator(factor=0.5)
row = layout.row()
row.scale_y = 1.2
row.operator(AvatarToolKit_OT_ConvertMaterials.bl_idname, icon='SHADING_TEXTURE')
+49
View File
@@ -0,0 +1,49 @@
# MMD Tools disabled for the time being unto it can be fixed.
# import bpy
# from typing import Set
# from bpy.types import Panel, Context, UILayout
# from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
# from ..core.translations import t
# class AvatarToolKit_PT_MMDPanel(Panel):
# """Panel containing MMD bone standardization and cleanup tools"""
# bl_label = t("MMD.label")
# bl_idname = "OBJECT_PT_avatar_toolkit_mmd"
# bl_space_type = 'VIEW_3D'
# bl_region_type = 'UI'
# bl_category = CATEGORY_NAME
# bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
# bl_order = 3
# bl_options = {'DEFAULT_CLOSED'}
# def draw(self, context: Context) -> None:
# layout: UILayout = self.layout
# toolkit = context.scene.avatar_toolkit
# Bone Settings Box
# bone_box: UILayout = layout.box()
# col: UILayout = bone_box.column(align=True)
# col.label(text=t("MMD.bone_settings"), icon='BONE_DATA')
# col.separator(factor=0.5)
# col.prop(toolkit, "keep_twist_bones")
# col.prop(toolkit, "keep_upper_chest")
# col.operator("avatar_toolkit.standardize_mmd", icon='BONE_DATA')
# Mesh Tools Box
# mesh_box: UILayout = layout.box()
# col = mesh_box.column(align=True)
# col.label(text=t("MMD.mesh_tools"), icon='MESH_DATA')
# col.separator(factor=0.5)
# row: UILayout = col.row(align=True)
# row.operator("avatar_toolkit.fix_meshes", icon='MODIFIER')
# row.operator("avatar_toolkit.validate_meshes", icon='CHECKMARK')
# Cleanup Box
# cleanup_box: UILayout = layout.box()
# col = cleanup_box.column(align=True)
# col.label(text=t("MMD.cleanup"), icon='BRUSH_DATA')
# col.separator(factor=0.5)
# col.operator("avatar_toolkit.cleanup_mmd", icon='SHADERFX')
# col.operator("avatar_toolkit.convert_mmd_morphs", icon='SHAPEKEY_DATA')
# col.operator("avatar_toolkit.reparent_meshes", icon='OUTLINER_OB_ARMATURE')
-50
View File
@@ -1,50 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..functions.translations import t
from ..functions.remove_doubles_safely import AvatarToolKit_OT_RemoveDoublesSafely, AvatarToolKit_OT_RemoveDoublesSafelyAdvanced
from ..core.common import get_selected_armature
from ..functions.mesh_tools import AvatarToolKit_OT_JoinAllMeshes, AvatarToolKit_OT_JoinSelectedMeshes
from ..functions.combine_materials import AvatarToolKit_OT_CombineMaterials
@register_wrap
class AvatarToolkit_PT_OptimizationPanel(bpy.types.Panel):
bl_label = t("Optimization.label")
bl_idname = "OBJECT_PT_avatar_toolkit_optimization"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 2
def draw(self: bpy.types.Panel, context: bpy.types.Context):
layout = self.layout
armature = get_selected_armature(context)
if armature:
layout.label(text=t("Optimization.options.label"), icon='SETTINGS')
layout.separator(factor=0.5)
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(AvatarToolKit_OT_CombineMaterials.bl_idname, text=t("Optimization.combine_materials.label"), icon='MATERIAL')
layout.separator(factor=0.5)
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(AvatarToolKit_OT_RemoveDoublesSafely.bl_idname, text=t("Optimization.remove_doubles_safely.label"), icon='SNAP_VERTEX')
row.operator(AvatarToolKit_OT_RemoveDoublesSafelyAdvanced.bl_idname, text=t("Optimization.remove_doubles_safely_advanced.label"), icon="ACTION")
layout.separator(factor=1.0)
layout.label(text=t("Optimization.joinmeshes.label"), icon='OBJECT_DATA')
row = layout.row(align=True)
row.scale_y = 1.2
row.operator(AvatarToolKit_OT_JoinAllMeshes.bl_idname, text=t("Optimization.join_all_meshes.label"), icon='OUTLINER_OB_MESH')
row.operator(AvatarToolKit_OT_JoinSelectedMeshes.bl_idname, text=t("Optimization.join_selected_meshes.label"), icon='STICKY_UVS_LOC')
else:
layout.label(text=t("Optimization.select_armature"), icon='ERROR')
+51
View File
@@ -0,0 +1,51 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
class AvatarToolKit_PT_OptimizationPanel(Panel):
"""Panel containing mesh and material optimization tools for avatar optimization"""
bl_label: str = t("Optimization.label")
bl_idname: str = "OBJECT_PT_avatar_toolkit_optimization"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 1
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draws the optimization panel interface with material, mesh cleanup and join mesh tools"""
layout: UILayout = self.layout
# Materials Box
materials_box: UILayout = layout.box()
col: UILayout = materials_box.column(align=True)
col.label(text=t("Optimization.materials_title"), icon='MATERIAL')
col.separator(factor=0.5)
# Material Operations
col.operator("avatar_toolkit.combine_materials", icon='MATERIAL')
# Mesh Cleanup Box
cleanup_box: UILayout = layout.box()
col: UILayout = cleanup_box.column(align=True)
col.label(text=t("Optimization.cleanup_title"), icon='MESH_DATA')
col.separator(factor=0.5)
# Remove Doubles Row
row: UILayout = col.row(align=True)
row.operator("avatar_toolkit.remove_doubles", icon='MESH_DATA')
row.operator("avatar_toolkit.remove_doubles_advanced", icon='PREFERENCES')
# Join Meshes Box
join_box: UILayout = layout.box()
col: UILayout = join_box.column(align=True)
col.label(text=t("Optimization.join_meshes_title"), icon='OBJECT_DATA')
col.separator(factor=0.5)
# Join Meshes Row
row: UILayout = col.row(align=True)
row.operator("avatar_toolkit.join_all_meshes", icon='OBJECT_DATA')
row.operator("avatar_toolkit.join_selected_meshes", icon='RESTRICT_SELECT_OFF')
-24
View File
@@ -1,24 +0,0 @@
import bpy
from ..core.register import register_wrap
from ..functions.translations import t
def draw_title(self: bpy.types.Panel):
layout = self.layout
layout.label(text=t("AvatarToolkit.desc1"))
layout.label(text=t("AvatarToolkit.desc2"))
layout.label(text=t("AvatarToolkit.desc3"))
CATEGORY_NAME = "Avatar Toolkit"
@register_wrap
class AvatarToolKit_PT_AvatarToolkitPanel(bpy.types.Panel):
bl_label = t("AvatarToolkit.label")
bl_idname = "OBJECT_PT_avatar_toolkit"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
def draw(self: bpy.types.Panel, context: bpy.types.Context):
draw_title(self)
-94
View File
@@ -1,94 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.export_resonite import AvatarToolKit_OT_ExportResonite
from bpy.types import Context, Mesh, Panel, Operator
from ..functions.translations import t
from ..core.import_pmx import import_pmx
from ..core.import_pmd import import_pmd
from ..functions.import_anything import AvatarToolKit_OT_ImportAnyModel
from ..functions.armature_modifying import AvatarToolkit_OT_StartPoseMode, AvatarToolkit_OT_StopPoseMode, AvatarToolkit_OT_ApplyPoseAsRest, AvatarToolkit_OT_ApplyPoseAsShapekey
from ..core.common import get_selected_armature, set_selected_armature, get_all_meshes
@register_wrap
class AvatarToolkitQuickAccessPanel(Panel):
bl_label = t("Quick_Access.label")
bl_idname = "OBJECT_PT_avatar_toolkit_quick_access"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 1
def draw(self, context: Context):
layout = self.layout
layout.label(text=t("Quick_Access.options"), icon='TOOL_SETTINGS')
layout.separator(factor=1.0)
layout.label(text=t("Quick_Access.select_armature"), icon='ARMATURE_DATA')
layout.prop(context.scene, "selected_armature", text="")
layout.separator(factor=1.0)
layout.label(text=t("Quick_Access.import_export.label"), icon='IMPORT')
row = layout.row(align=True)
row.scale_y = 1.5
row.operator(AvatarToolKit_OT_ImportAnyModel.bl_idname, text=t("Quick_Access.import"), icon='IMPORT')
row.operator(AVATAR_TOOLKIT_OT_ExportMenu.bl_idname, text=t("Quick_Access.export"), icon='EXPORT')
layout.separator(factor=1.0)
if get_selected_armature(context) != None:
if(context.mode == "POSE"):
col = layout.column(align=True)
col.scale_y = 1.2
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, text=t("Quick_Access.stop_pose_mode.label"), icon='POSE_HLT')
layout.separator(factor=0.5)
col = layout.column(align=True)
col.scale_y = 1.2
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, text=t("Quick_Access.apply_pose_as_rest.label"), icon='MOD_ARMATURE')
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, text=t("Quick_Access.apply_pose_as_shapekey.label"), icon='MOD_ARMATURE')
else:
row = layout.row()
row.scale_y = 1.2
row.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, text=t("Quick_Access.start_pose_mode.label"), icon='POSE_HLT')
@register_wrap
class AVATAR_TOOLKIT_OT_ExportMenu(bpy.types.Operator):
bl_idname = "avatar_toolkit.export_menu"
bl_label = t("Quick_Access.export_menu.label")
bl_description = t("Quick_Access.export_menu.desc")
@classmethod
def poll(cls, context):
return any(obj.type == 'MESH' for obj in context.scene.objects)
def execute(self, context: Context) -> set[str]:
return {'FINISHED'}
def invoke(self, context: Context, event):
wm = context.window_manager
return wm.invoke_popup(self, width=200)
def draw(self, context: Context):
layout = self.layout
layout.label(text=t("Quick_Access.select_export.label"), icon='EXPORT')
layout.operator(AvatarToolKit_OT_ExportResonite.bl_idname, text=t("Quick_Access.select_export_resonite.label"), icon='SCENE_DATA')
layout.operator(AVATAR_TOOLKIT_OT_ExportFbx.bl_idname, text=t("Quick_Access.export_fbx.label"), icon='OBJECT_DATA')
@register_wrap
class AVATAR_TOOLKIT_OT_ExportFbx(bpy.types.Operator):
bl_idname = 'avatar_toolkit.export_fbx'
bl_label = t("Quick_Access.export_fbx.label")
bl_description = t("Quick_Access.export_fbx.desc")
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context) -> set[str]:
bpy.ops.export_scene.fbx('INVOKE_DEFAULT')
return {'FINISHED'}
+154
View File
@@ -0,0 +1,154 @@
import bpy
from typing import Set, Dict, List, Optional, Tuple
from bpy.types import (
Operator,
Panel,
Menu,
Context,
UILayout,
WindowManager,
Object
)
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
from ..core.common import (
get_active_armature,
clear_default_objects,
validate_armature,
get_armature_list,
get_armature_stats
)
from ..core.importers.importer import import_types, imports
from ..functions.pose_mode import (
AvatarToolkit_OT_StartPoseMode,
AvatarToolkit_OT_StopPoseMode,
AvatarToolkit_OT_ApplyPoseAsShapekey,
AvatarToolkit_OT_ApplyPoseAsRest
)
class AvatarToolKit_OT_Import(Operator):
"""Import FBX files into Blender with Avatar Toolkit settings"""
bl_idname: str = "avatar_toolkit.import"
bl_label: str = t("QuickAccess.import")
def execute(self, context: Context) -> Set[str]:
clear_default_objects()
bpy.ops.import_scene.fbx('INVOKE_DEFAULT', filter_glob=imports)
return {'FINISHED'}
class AvatarToolKit_OT_ExportFBX(Operator):
"""Export selected objects as FBX"""
bl_idname: str = "avatar_toolkit.export_fbx"
bl_label: str = t("QuickAccess.export_fbx")
def execute(self, context: Context) -> Set[str]:
bpy.ops.export_scene.fbx('INVOKE_DEFAULT')
return {'FINISHED'}
class AvatarToolKit_MT_ExportMenu(Menu):
"""Export menu containing various export options"""
bl_idname: str = "AVATAR_TOOLKIT_MT_export_menu"
bl_label: str = t("QuickAccess.export")
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.operator("avatar_toolkit.export_fbx", text=t("QuickAccess.export_fbx"))
layout.operator("avatar_toolkit.export_resonite", text=t("QuickAccess.export_resonite"))
class AvatarToolKit_OT_ExportMenu(Operator):
"""Open the export menu"""
bl_idname: str = "avatar_toolkit.export"
bl_label: str = t("QuickAccess.export")
def execute(self, context: Context) -> Set[str]:
wm: WindowManager = context.window_manager
wm.call_menu(name=AvatarToolKit_MT_ExportMenu.bl_idname)
return {'FINISHED'}
class AvatarToolKit_PT_QuickAccessPanel(Panel):
"""Quick access panel for common Avatar Toolkit operations"""
bl_label: str = t("QuickAccess.label")
bl_idname: str = "OBJECT_PT_avatar_toolkit_quick_access"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 0
def draw(self, context: Context) -> None:
"""Draw the panel layout"""
layout: UILayout = self.layout
# Armature Selection Box
armature_box: UILayout = layout.box()
col: UILayout = armature_box.column(align=True)
col.label(text=t("QuickAccess.select_armature"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
# Armature Selection
col.prop(context.scene.avatar_toolkit, "active_armature", text="")
# Armature Validation
active_armature: Optional[Object] = get_active_armature(context)
if active_armature:
is_valid: bool
messages: List[str]
is_valid, messages = validate_armature(active_armature)
# Create info box for all validation information
info_box: UILayout = col.box()
if is_valid:
row: UILayout = info_box.row()
split: UILayout = row.split(factor=0.6)
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK')
stats: Dict[str, int] = get_armature_stats(active_armature)
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
if stats['has_pose']:
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
else:
# Display validation failure messages
for message in messages:
info_box.label(text=message, icon='ERROR')
# Validation Mode Warnings - always show in info box
validation_mode = context.scene.avatar_toolkit.validation_mode
if validation_mode == 'BASIC':
warning_row = info_box.box()
warning_row.alert = True
warning_row.label(text=t("QuickAccess.validation_basic_warning"), icon='INFO')
warning_row.label(text=t("QuickAccess.validation_basic_details"))
elif validation_mode == 'NONE':
warning_row = info_box.box()
warning_row.alert = True
warning_row.label(text=t("QuickAccess.validation_none_warning"), icon='ERROR')
warning_row.label(text=t("QuickAccess.validation_none_details"))
# Pose Mode Controls
pose_box: UILayout = layout.box()
col = pose_box.column(align=True)
col.label(text=t("QuickAccess.pose_controls"), icon='ARMATURE_DATA')
col.separator(factor=0.5)
if context.mode == "POSE":
col.operator(AvatarToolkit_OT_StopPoseMode.bl_idname, icon='POSE_HLT')
col.separator(factor=0.5)
col.operator(AvatarToolkit_OT_ApplyPoseAsRest.bl_idname, icon='MOD_ARMATURE')
col.operator(AvatarToolkit_OT_ApplyPoseAsShapekey.bl_idname, icon='MOD_ARMATURE')
else:
col.operator(AvatarToolkit_OT_StartPoseMode.bl_idname, icon='POSE_HLT')
# Import/Export Box
import_box: UILayout = layout.box()
col = import_box.column(align=True)
col.label(text=t("QuickAccess.import_export"), icon='IMPORT')
col.separator(factor=0.5)
# Import/Export Buttons
button_row: UILayout = col.row(align=True)
button_row.scale_y = 1.5
button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT')
button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT')
-42
View File
@@ -1,42 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..functions.translations import t
@register_wrap
class AvatarToolkitSettingsPanel(bpy.types.Panel):
bl_label = t("Settings.label")
bl_idname = "OBJECT_PT_avatar_toolkit_settings"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 8
def draw(self, context):
layout = self.layout
layout.label(text=t("Settings.language.label"))
layout.prop(context.scene, "avatar_toolkit_language", text="", icon='WORLD')
@register_wrap
class AVATAR_TOOLKIT_OT_translation_restart_popup(bpy.types.Operator):
bl_idname = "avatar_toolkit.translation_restart_popup"
bl_label = t("Settings.translation_restart_popup.label")
bl_description = t("Settings.translation_restart_popup.description")
bl_options = {'INTERNAL'}
def execute(self, context):
if context.scene.avatar_toolkit_language_changed:
bpy.ops.script.reload()
context.scene.avatar_toolkit_language_changed = False
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width=300)
def draw(self, context):
layout = self.layout
layout.label(text=t("Settings.translation_restart_popup.message1"), icon='INFO')
layout.label(text=t("Settings.translation_restart_popup.message2"), icon='FILE_REFRESH')
+76
View File
@@ -0,0 +1,76 @@
import bpy
from typing import Set, Dict, List, Optional
from bpy.types import (
Operator,
Panel,
Context,
UILayout,
WindowManager,
Event
)
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t, get_languages_list
class AvatarToolkit_OT_TranslationRestartPopup(Operator):
"""Popup dialog shown after language change to inform about restart requirement"""
bl_idname: str = "avatar_toolkit.translation_restart_popup"
bl_label: str = t("Language.changed.title")
def execute(self, context: Context) -> Set[str]:
return {'FINISHED'}
def invoke(self, context: Context, event: Event) -> Set[str]:
wm: WindowManager = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context: Context) -> None:
layout: UILayout = self.layout
layout.label(text=t("Language.changed.success"))
layout.label(text=t("Language.changed.restart"))
class AvatarToolKit_PT_SettingsPanel(Panel):
"""Settings panel for Avatar Toolkit containing language preferences"""
bl_label: str = t("Settings.label")
bl_idname: str = "OBJECT_PT_avatar_toolkit_settings"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 7
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the settings panel layout with language selection"""
layout: UILayout = self.layout
# Language Settings
lang_box: UILayout = layout.box()
col: UILayout = lang_box.column(align=True)
row: UILayout = col.row()
row.scale_y = 1.2
row.label(text=t("Settings.language"), icon='WORLD')
col.separator()
col.prop(context.scene.avatar_toolkit, "language", text="")
# Validation Settings
val_box: UILayout = layout.box()
col = val_box.column(align=True)
row = col.row()
row.scale_y = 1.2
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
col.separator()
col.prop(context.scene.avatar_toolkit, "validation_mode", text="")
# Debug Settings
debug_box = layout.box()
col = debug_box.column()
row = col.row(align=True)
row.prop(context.scene.avatar_toolkit, "debug_expand",
icon="TRIA_DOWN" if context.scene.avatar_toolkit.debug_expand
else "TRIA_RIGHT",
icon_only=True, emboss=False)
row.label(text=t("Settings.debug"), icon='CONSOLE')
if context.scene.avatar_toolkit.debug_expand:
col = debug_box.column(align=True)
col.prop(context.scene.avatar_toolkit, "enable_logging")
-76
View File
@@ -1,76 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from bpy.types import Context
from ..functions.digitigrade_legs import AvatarToolKit_OT_CreateDigitigradeLegs
from ..functions.resonite_functions import AvatarToolKit_OT_ConvertToResonite
from ..functions.translations import t
from ..core.common import get_selected_armature
from ..functions.mesh_tools import AvatarToolkit_OT_RemoveUnusedShapekeys
from ..functions.additional_tools import AvatarToolKit_OT_ApplyTransforms, AvatarToolKit_OT_ConnectBones, AvatarToolKit_OT_DeleteBoneConstraints, AvatarToolKit_OT_SeparateByMaterials, AvatarToolKit_OT_SeparateByLooseParts
from ..functions.armature_modifying import AvatarToolkit_OT_RemoveZeroWeightBones, AvatarToolkit_OT_MergeBonesToActive, AvatarToolkit_OT_MergeBonesToParents
from ..functions.rigify_functions import AvatarToolKit_OT_ConvertRigifyToUnity
@register_wrap
class AvatarToolkit_PT_ToolsPanel(bpy.types.Panel):
bl_label = t("Tools.label")
bl_idname = "OBJECT_PT_avatar_toolkit_tools"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 3
def draw(self, context: Context):
layout = self.layout
armature = get_selected_armature(context)
if armature:
layout.label(text=t("Tools.tools_title.label"), icon='TOOL_SETTINGS')
layout.separator(factor=0.5)
row = layout.row(align=True)
row.scale_y = 1.5
row.operator(AvatarToolKit_OT_ConvertToResonite.bl_idname, text=t("Tools.convert_to_resonite.label"), icon='SCENE_DATA')
row = layout.row(align=True)
row.scale_y = 1.5
row.operator(AvatarToolKit_OT_ConvertRigifyToUnity.bl_idname, text=t("Tools.convert_rigify_to_unity.label"), icon='ARMATURE_DATA')
layout.separator(factor=1.0)
layout.label(text=t("Tools.separate_by.label"), icon='MESH_DATA')
row = layout.row(align=True)
row.operator(AvatarToolKit_OT_SeparateByMaterials.bl_idname, text=t("Tools.separate_by_materials.label"), icon='MATERIAL')
row.operator(AvatarToolKit_OT_SeparateByLooseParts.bl_idname, text=t("Tools.separate_by_loose_parts.label"), icon='OUTLINER_OB_MESH')
layout.separator(factor=1.0)
layout.label(text=t("Tools.bone_tools.label"), icon='BONE_DATA')
row = layout.row(align=True)
row.operator(AvatarToolKit_OT_CreateDigitigradeLegs.bl_idname, text=t("Tools.create_digitigrade_legs.label"), icon='BONE_DATA')
row = layout.row(align=True)
row.operator(AvatarToolkit_OT_RemoveZeroWeightBones.bl_idname, text=t("Tools.remove_zero_weight_bones.label"), icon='BONE_DATA')
row = layout.row(align=True)
row.operator(AvatarToolkit_OT_MergeBonesToActive.bl_idname, text=t("Tools.merge_bones_to_active.label"), icon='BONE_DATA')
row.operator(AvatarToolkit_OT_MergeBonesToParents.bl_idname, text=t("Tools.merge_bones_to_parents.label"), icon='BONE_DATA')
row = layout.row(align=True)
row.operator(AvatarToolKit_OT_ConnectBones.bl_idname, text=t("Tools.connect_bones.label"), icon='BONE_DATA')
row.operator(AvatarToolKit_OT_DeleteBoneConstraints.bl_idname, text=t("Tools.delete_bone_constraints.label"), icon='CONSTRAINT_BONE')
row = layout.row()
row.prop(context.scene, "merge_twist_bones")
layout.separator(factor=1.0)
layout.label(text=t("Tools.additional_tools.label"), icon='TOOL_SETTINGS')
row = layout.row(align=True)
row.operator(AvatarToolKit_OT_ApplyTransforms.bl_idname, text=t("Tools.apply_transforms.label"), icon='OBJECT_ORIGIN')
row.operator(AvatarToolkit_OT_RemoveUnusedShapekeys.bl_idname, text=t("Tools.remove_unused_shapekeys.label"), icon='SHAPEKEY_DATA')
layout.separator(factor=1.0)
else:
layout.label(text=t("Tools.select_armature"), icon='ERROR')
+69
View File
@@ -0,0 +1,69 @@
import bpy
from typing import Set
from bpy.types import Panel, Context, UILayout, Operator
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..core.translations import t
class AvatarToolKit_PT_ToolsPanel(Panel):
"""Panel containing various tools for avatar customization and optimization"""
bl_label: str = t("Tools.label")
bl_idname: str = "OBJECT_PT_avatar_toolkit_tools"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 2
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the tools panel interface"""
layout: UILayout = self.layout
# General Tools
tools_box: UILayout = layout.box()
col: UILayout = tools_box.column(align=True)
col.label(text=t("Tools.general_title"), icon='TOOL_SETTINGS')
col.separator(factor=0.5)
col.operator("avatar_toolkit.convert_resonite", text=t("Tools.convert_resonite"), icon='EXPORT')
# Separation Tools
sep_box: UILayout = layout.box()
col = sep_box.column(align=True)
col.label(text=t("Tools.separate_title"), icon='MOD_EXPLODE')
col.separator(factor=0.5)
row: UILayout = col.row(align=True)
row.operator("avatar_toolkit.separate_materials", text=t("Tools.separate_materials"), icon='MATERIAL')
row.operator("avatar_toolkit.separate_loose", text=t("Tools.separate_loose"), icon='MESH_DATA')
# Bone Tools
bone_box: UILayout = layout.box()
col = bone_box.column(align=True)
col.label(text=t("Tools.bone_title"), icon='BONE_DATA')
col.separator(factor=0.5)
col.operator("avatar_toolkit.create_digitigrade", text=t("Tools.create_digitigrade"), icon='BONE_DATA')
# Weight Tools
weight_box: UILayout = bone_box.box()
col = weight_box.column(align=True)
col.prop(context.scene.avatar_toolkit, "merge_twist_bones", text=t("Tools.merge_twist_bones"))
row = col.row(align=True)
row.operator("avatar_toolkit.clean_weights", text=t("Tools.clean_weights"), icon='GROUP_BONE')
row.operator("avatar_toolkit.clean_constraints", text=t("Tools.clean_constraints"), icon='CONSTRAINT_BONE')
# Merge Tools
merge_box: UILayout = layout.box()
col = merge_box.column(align=True)
col.label(text=t("Tools.merge_title"), icon='AUTOMERGE_ON')
col.separator(factor=0.5)
row = col.row(align=True)
row.operator("avatar_toolkit.merge_to_active", text=t("Tools.merge_to_active"), icon='BONE_DATA')
row.operator("avatar_toolkit.merge_to_parent", text=t("Tools.merge_to_parent"), icon='BONE_DATA')
col.operator("avatar_toolkit.connect_bones", text=t("Tools.connect_bones"), icon='BONE_DATA')
# Additional Tools
extra_box: UILayout = layout.box()
col = extra_box.column(align=True)
col.label(text=t("Tools.additional_title"), icon='TOOL_SETTINGS')
col.separator(factor=0.5)
col.operator("avatar_toolkit.apply_transforms", text=t("Tools.apply_transforms"), icon='OBJECT_DATA')
col.operator("avatar_toolkit.clean_shapekeys", text=t("Tools.clean_shapekeys"), icon='SHAPEKEY_DATA')
-17
View File
@@ -1,17 +0,0 @@
import bpy
from ..core.register import register_wrap
from ..functions.translations import t
from .panel import draw_title
@register_wrap
class UVTools_PT_MainPanel(bpy.types.Panel):
bl_label = t("AvatarToolkit.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
def draw(self: bpy.types.Panel, context: bpy.types.Context):
layout = self.layout
draw_title(self)
-23
View File
@@ -1,23 +0,0 @@
import bpy
from ..core.register import register_wrap
from ..functions.translations import t
from ..functions.uv_tools import AvatarToolkit_OT_AlignUVEdgesToTarget
from .panel import draw_title
from .uv_panel import UVTools_PT_MainPanel
@register_wrap
class UVTools_PT_Tools(bpy.types.Panel):
bl_label = t("Tools.label")
bl_idname = "OBJECT_PT_avatar_toolkit_uv_tools"
bl_space_type = 'IMAGE_EDITOR'
bl_region_type = 'UI'
bl_category = "Avatar Toolkit"
bl_parent_id = UVTools_PT_MainPanel.bl_idname
bl_order = 3
def draw(self, context: bpy.types.Context):
layout = self.layout
row = layout.row(align=True)
row.operator(AvatarToolkit_OT_AlignUVEdgesToTarget.bl_idname, text=t("avatar_toolkit.align_uv_edges_to_target.label"), icon='GP_MULTIFRAME_EDITING')
-56
View File
@@ -1,56 +0,0 @@
import bpy
from ..core.register import register_wrap
from .panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
from ..functions.viseme import AvatarToolKit_OT_AutoVisemeButton
from ..functions.translations import t
from ..core.common import get_selected_armature
@register_wrap
class AvatarToolkitVisemePanel(bpy.types.Panel):
bl_label = t("VisemePanel.label")
bl_idname = "OBJECT_PT_avatar_toolkit_viseme"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = CATEGORY_NAME
bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order = 7
def draw(self, context: bpy.types.Context) -> None:
layout = self.layout
armature = get_selected_armature(context)
if armature:
layout.label(text=t("VisemePanel.label"), icon='SOUND')
layout.separator(factor=0.5)
layout.prop(context.scene, "selected_mesh", text=t("VisemePanel.select_mesh"), icon='OUTLINER_OB_MESH')
mesh = bpy.data.objects.get(context.scene.selected_mesh)
if mesh and mesh.type == 'MESH':
if mesh.data.shape_keys:
box = layout.box()
col = box.column(align=True)
col.prop_search(context.scene, "avatar_toolkit_mouth_a", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_a.label'), icon='SHAPEKEY_DATA')
col.prop_search(context.scene, "avatar_toolkit_mouth_o", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_o.label'), icon='SHAPEKEY_DATA')
col.prop_search(context.scene, "avatar_toolkit_mouth_ch", mesh.data.shape_keys, "key_blocks", text=t('VisemePanel.mouth_ch.label'), icon='SHAPEKEY_DATA')
layout.separator(factor=0.5)
layout.prop(context.scene, 'avatar_toolkit_shape_intensity', text=t('VisemePanel.shape_intensity'), icon='FORCE_LENNARDJONES')
layout.separator(factor=1.0)
row = layout.row()
row.scale_y = 1.5
row.operator(AvatarToolKit_OT_AutoVisemeButton.bl_idname, text=t('VisemePanel.create_visemes'), icon='TRIA_RIGHT')
else:
layout.label(text=t('VisemePanel.error.noShapekeys'), icon='ERROR')
else:
layout.label(text=t('VisemePanel.error.selectMesh'), icon='INFO')
else:
layout.label(text=t('VisemePanel.error.noArmature'), icon='ERROR')
layout.separator(factor=1.0)
layout.label(text=t('VisemePanel.info.selectMesh'), icon='HELP')
+60
View File
@@ -0,0 +1,60 @@
from bpy.types import Panel, Context, UILayout
from ..core.translations import t
from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME
class AvatarToolKit_PT_VisemesPanel(Panel):
"""Panel containing viseme creation and preview tools"""
bl_label: str = t("Visemes.panel_label")
bl_idname: str = "VIEW3D_PT_avatar_toolkit_visemes"
bl_space_type: str = 'VIEW_3D'
bl_region_type: str = 'UI'
bl_category: str = CATEGORY_NAME
bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname
bl_order: int = 5
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
"""Draw the visemes panel interface"""
layout: UILayout = self.layout
props = context.scene.avatar_toolkit
# Check for valid mesh with shape keys
if not context.active_object or context.active_object.type != 'MESH' or not context.active_object.data.shape_keys:
layout.label(text=t("Visemes.no_shapekeys"))
return
# Shape Key Selection Box
shape_box: UILayout = layout.box()
col: UILayout = shape_box.column(align=True)
col.label(text=t("Visemes.shape_selection"), icon='SHAPEKEY_DATA')
col.separator(factor=0.5)
# Shape key selection with valid data
shape_keys = context.active_object.data.shape_keys
col.prop_search(props, "mouth_a", shape_keys, "key_blocks", text=t("Visemes.mouth_a"))
col.prop_search(props, "mouth_o", shape_keys, "key_blocks", text=t("Visemes.mouth_o"))
col.prop_search(props, "mouth_ch", shape_keys, "key_blocks", text=t("Visemes.mouth_ch"))
# Shape intensity slider
col.separator()
col.prop(props, "shape_intensity", slider=True)
# Preview Box
preview_box: UILayout = layout.box()
col = preview_box.column(align=True)
col.label(text=t("Visemes.preview_label"), icon='HIDE_OFF')
col.separator(factor=0.5)
if props.viseme_preview_mode:
col.prop(props, "viseme_preview_selection", text="")
col.separator()
preview_text = t("Visemes.stop_preview") if props.viseme_preview_mode else t("Visemes.start_preview")
col.operator("avatar_toolkit.preview_visemes", text=preview_text, icon='HIDE_OFF')
# Create Box
create_box: UILayout = layout.box()
col = create_box.column(align=True)
col.label(text=t("Visemes.create_label"), icon='ADD')
col.separator(factor=0.5)
col.operator("avatar_toolkit.create_visemes", icon='ADD')