From 02c73ccd2a5657c020aecea431f717a3bf877998 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 27 Mar 2025 18:00:46 +0000 Subject: [PATCH 01/20] Auto load now works with custom blender extensions folders - Enable autoload, load things from outside the default blender folder, before is a user tried to install the plugin outside from blender default extension folder it will error. I found this out when i broke my own blender helping someone. --- core/auto_load.py | 68 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 64 insertions(+), 4 deletions(-) diff --git a/core/auto_load.py b/core/auto_load.py index 730daf1..6e6727e 100644 --- a/core/auto_load.py +++ b/core/auto_load.py @@ -39,7 +39,14 @@ def init() -> None: def register() -> None: """Register all discovered classes and modules""" + global modules, ordered_classes + print("Registering classes") + + if not ordered_classes: + print("Warning: No classes to register") + ordered_classes = [] + for cls in ordered_classes: print(f"Registering: {cls}") try: @@ -47,6 +54,10 @@ def register() -> None: except ValueError: continue + if not modules: + print("Warning: No modules to register") + modules = [] + for module in modules: if module.__name__ == __name__: continue @@ -77,18 +88,67 @@ def get_manifest_id() -> str: def get_all_submodules(directory: Path) -> List[Any]: """Discover and import all submodules in the given directory""" modules = [] - addon_id = get_manifest_id() + + addon_folder_name = directory.name + + # Add the parent directory to sys.path so Python can find our module + parent_dir = str(directory.parent) + if parent_dir not in sys.path: + sys.path.append(parent_dir) + print(f"Added {parent_dir} to sys.path") + + # Try to detect if we're in the default Blender extension path + is_default_path = False + try: + import bl_ext.user_default + is_default_path = True + print("Detected default Blender extension path") + except ImportError: + print("Using custom installation path") + 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}" + if is_default_path: + package_name = f"bl_ext.user_default.{addon_folder_name}" + else: + package_name = addon_folder_name else: relative_path = path.relative_to(directory).as_posix().replace('/', '.') - package_name = f"bl_ext.user_default.{addon_id}.{relative_path}" + if is_default_path: + package_name = f"bl_ext.user_default.{addon_folder_name}.{relative_path}" + else: + package_name = f"{addon_folder_name}.{relative_path}" + for name in sorted(iter_module_names(path)): - modules.append(importlib.import_module(f".{name}", package_name)) + if is_default_path: + try: + # First try the default Blender extension path + module = importlib.import_module(f"{package_name}.{name}") + modules.append(module) + print(f"Successfully imported {name} from {package_name}") + except ImportError as e: + # Fall back to direct import + try: + direct_package = f"{addon_folder_name}.{relative_path}" if path != directory else addon_folder_name + module = importlib.import_module(f"{direct_package}.{name}") + modules.append(module) + print(f"Successfully imported {name} from {direct_package} (fallback)") + except ImportError as e2: + print(f"Error importing {name}: {e} / {e2}") + else: + # For custom path, just use direct import + try: + module = importlib.import_module(f"{package_name}.{name}") + modules.append(module) + print(f"Successfully imported {name} from {package_name}") + except ImportError as e: + print(f"Error importing {name} from {package_name}: {e}") + return modules def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]: From 357aa1b6d90067f65aa078c6ad9773abcf9afeb7 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 27 Mar 2025 20:16:27 +0000 Subject: [PATCH 02/20] Let's not use System path --- __init__.py | 23 +++--- core/auto_load.py | 176 ++++++++++++++++------------------------------ 2 files changed, 72 insertions(+), 127 deletions(-) diff --git a/__init__.py b/__init__.py index ee17c15..103e6a7 100644 --- a/__init__.py +++ b/__init__.py @@ -13,7 +13,6 @@ def show_version_error_popup(): bpy.context.window_manager.popup_menu(draw, title="Avatar Toolkit Version Error", icon='ERROR') def register(): - # Check Blender version first import bpy version = bpy.app.version if version[0] > 4 or (version[0] == 4 and version[1] >= 5): @@ -24,27 +23,29 @@ def register(): try: import lz4 except ImportError: - import sys import os import site import pip wheels_dir = os.path.join(os.path.dirname(__file__), "wheels") - for wheel in os.listdir(wheels_dir): - if wheel.endswith(".whl"): - pip.main(['install', os.path.join(wheels_dir, wheel)]) - site.addsitedir(site.getsitepackages()[0]) + if os.path.exists(wheels_dir): + for wheel in os.listdir(wheels_dir): + if wheel.endswith(".whl"): + pip.main(['install', os.path.join(wheels_dir, wheel)]) + + # Refresh site packages without modifying sys.path + site.addsitedir(site.getsitepackages()[0]) - from .core import auto_load print("Starting registration") - # Make sure to initialize logging first + # Import modules using relative imports + from . import core + from .core import auto_load from .core.logging_setup import configure_logging + + # Initialize logging configure_logging(False) - # Then initialize the addon auto_load.init() - - # Register classes in proper order auto_load.register() # Verify property registration diff --git a/core/auto_load.py b/core/auto_load.py index 6e6727e..4925f91 100644 --- a/core/auto_load.py +++ b/core/auto_load.py @@ -1,6 +1,5 @@ import os import bpy -import sys import typing import inspect import pkgutil @@ -24,7 +23,6 @@ def init() -> None: global modules global ordered_classes - # Configure logging first from .logging_setup import configure_logging configure_logging(False) @@ -32,7 +30,10 @@ def init() -> None: configure_logging(get_preference("enable_logging", False)) print("Auto-load init starting") - modules = get_all_submodules(Path(__file__).parent.parent) + + package_name = __package__.rsplit('.', 1)[0] + directory = Path(__file__).parent.parent + modules = get_all_submodules(directory, package_name) ordered_classes = get_ordered_classes_to_register(modules) print(f"Found modules: {modules}") print(f"Found classes: {ordered_classes}") @@ -78,93 +79,29 @@ def unregister() -> None: 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]: +def get_all_submodules(directory: Path, package_name: str) -> List[Any]: """Discover and import all submodules in the given directory""" - modules = [] - - addon_folder_name = directory.name - - # Add the parent directory to sys.path so Python can find our module - parent_dir = str(directory.parent) - if parent_dir not in sys.path: - sys.path.append(parent_dir) - print(f"Added {parent_dir} to sys.path") - - # Try to detect if we're in the default Blender extension path - is_default_path = False - try: - import bl_ext.user_default - is_default_path = True - print("Detected default Blender extension path") - except ImportError: - print("Using custom installation path") - - for root, dirs, files in os.walk(directory): - if "__pycache__" in root: - continue - - path = Path(root) - - if path == directory: - if is_default_path: - package_name = f"bl_ext.user_default.{addon_folder_name}" - else: - package_name = addon_folder_name - else: - relative_path = path.relative_to(directory).as_posix().replace('/', '.') - if is_default_path: - package_name = f"bl_ext.user_default.{addon_folder_name}.{relative_path}" - else: - package_name = f"{addon_folder_name}.{relative_path}" - - for name in sorted(iter_module_names(path)): - if is_default_path: - try: - # First try the default Blender extension path - module = importlib.import_module(f"{package_name}.{name}") - modules.append(module) - print(f"Successfully imported {name} from {package_name}") - except ImportError as e: - # Fall back to direct import - try: - direct_package = f"{addon_folder_name}.{relative_path}" if path != directory else addon_folder_name - module = importlib.import_module(f"{direct_package}.{name}") - modules.append(module) - print(f"Successfully imported {name} from {direct_package} (fallback)") - except ImportError as e2: - print(f"Error importing {name}: {e} / {e2}") - else: - # For custom path, just use direct import - try: - module = importlib.import_module(f"{package_name}.{name}") - modules.append(module) - print(f"Successfully imported {name} from {package_name}") - except ImportError as e: - print(f"Error importing {name} from {package_name}: {e}") - - return modules + return list(iter_submodules(directory, package_name)) -def iter_submodules(path: Path, package_name: str) -> Generator[Any, None, None]: +def iter_submodules(directory: 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) + for name in sorted(iter_submodule_names(directory)): + try: + yield importlib.import_module("." + name, package_name) + print(f"Successfully imported {name} from {package_name}") + except ImportError as e: + print(f"Error importing {name} from {package_name}: {e}") -def iter_module_names(path: Path) -> Generator[str, None, None]: +def iter_submodule_names(path: Path, root: str = "") -> 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 + for _, module_name, is_package in pkgutil.iter_modules([str(path)]): + if is_package: + sub_path = path / module_name + sub_root = root + module_name + "." + yield from iter_submodule_names(sub_path, sub_root) + else: + yield root + module_name def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]: """Get a topologically sorted list of classes to register""" @@ -172,28 +109,44 @@ def get_ordered_classes_to_register(modules: List[Any]) -> List[Type]: def get_register_deps_dict(modules: List[Any]) -> Dict[Type, Set[Type]]: """Get dependencies dictionary for class registration""" + my_classes = set(iter_classes_to_register(modules)) + my_classes_by_idname = {cls.bl_idname: cls for cls in my_classes if hasattr(cls, "bl_idname")} + 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)) + for cls in my_classes: + deps_dict[cls] = set() + deps_dict[cls].update(iter_deps_from_annotations(cls, my_classes)) + deps_dict[cls].update(iter_deps_from_parent_id(cls, my_classes_by_idname)) + 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""" +def iter_deps_from_annotations(cls: Type, my_classes: Set[Type]) -> Generator[Type, None, None]: + """Iterate through dependencies from class annotations""" for value in typing.get_type_hints(cls, {}, {}).values(): dependency = get_dependency_from_annotation(value) - if dependency is not None: + if dependency is not None and dependency in my_classes: yield dependency +def iter_deps_from_parent_id(cls: Type, my_classes_by_idname: Dict[str, Type]) -> Generator[Type, None, None]: + """Iterate through dependencies from panel parent IDs""" + if bpy.types.Panel in cls.__bases__: + parent_idname = getattr(cls, "bl_parent_id", None) + if parent_idname is not None: + parent_cls = my_classes_by_idname.get(parent_idname) + if parent_cls is not None: + yield parent_cls + 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"] + blender_version = bpy.app.version + + if blender_version >= (2, 93): + if isinstance(value, bpy.props._PropertyDeferred): + return value.keywords.get("type") + else: + 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]: @@ -224,7 +177,8 @@ def get_register_base_types() -> Set[Type]: "Panel", "Operator", "PropertyGroup", "AddonPreferences", "Header", "Menu", "Node", "NodeSocket", "NodeTree", - "UIList", "RenderEngine" + "UIList", "RenderEngine", + "Gizmo", "GizmoGroup", ]) def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]: @@ -232,25 +186,15 @@ def toposort(deps_dict: Dict[Type, Set[Type]]) -> List[Type]: 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): + while len(deps_dict) > 0: 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) + 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} return sorted_list From d7fee2c961496977a47a5cbfa8885cced7d9215f Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 27 Mar 2025 20:42:43 +0000 Subject: [PATCH 03/20] I don't need to add that check duh --- core/auto_load.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/core/auto_load.py b/core/auto_load.py index 4925f91..8c4a7f2 100644 --- a/core/auto_load.py +++ b/core/auto_load.py @@ -138,15 +138,9 @@ def iter_deps_from_parent_id(cls: Type, my_classes_by_idname: Dict[str, Type]) - def get_dependency_from_annotation(value: Any) -> Optional[Type]: """Get dependency type from a type annotation""" - blender_version = bpy.app.version - - if blender_version >= (2, 93): - if isinstance(value, bpy.props._PropertyDeferred): - return value.keywords.get("type") - else: - if isinstance(value, tuple) and len(value) == 2: - if value[0] in (bpy.props.PointerProperty, bpy.props.CollectionProperty): - return value[1]["type"] + 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]: From 9a0521dad5cfc25efd355dad1e0e3fa6992c9f36 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 27 Mar 2025 20:44:39 +0000 Subject: [PATCH 04/20] Fix because i stupid --- core/auto_load.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/auto_load.py b/core/auto_load.py index 8c4a7f2..dc326e6 100644 --- a/core/auto_load.py +++ b/core/auto_load.py @@ -138,9 +138,8 @@ def iter_deps_from_parent_id(cls: Type, my_classes_by_idname: Dict[str, Type]) - 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"] + if isinstance(value, bpy.props._PropertyDeferred): + return value.keywords.get("type") return None def iter_classes_to_register(modules: List[Any]) -> Generator[Type, None, None]: From 77b7b429a57c54d3aa50d89f96969875bc1ff5ff Mon Sep 17 00:00:00 2001 From: Yusarina Date: Thu, 27 Mar 2025 21:05:59 +0000 Subject: [PATCH 05/20] Remove wheel installation - Blender should handle this for us, we were installing system wide which is bad. --- __init__.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/__init__.py b/__init__.py index 103e6a7..dedf6b3 100644 --- a/__init__.py +++ b/__init__.py @@ -18,23 +18,7 @@ def register(): if version[0] > 4 or (version[0] == 4 and version[1] >= 5): show_version_error_popup() return - - # Add wheel installation check - try: - import lz4 - except ImportError: - import os - import site - import pip - wheels_dir = os.path.join(os.path.dirname(__file__), "wheels") - if os.path.exists(wheels_dir): - for wheel in os.listdir(wheels_dir): - if wheel.endswith(".whl"): - pip.main(['install', os.path.join(wheels_dir, wheel)]) - - # Refresh site packages without modifying sys.path - site.addsitedir(site.getsitepackages()[0]) - + print("Starting registration") # Import modules using relative imports From 334f299e0e5307e467be1e29eabf8c4884ef6040 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 13:03:37 +0100 Subject: [PATCH 06/20] User Preferences should save in blenders user dictionary not in the Plugins Dictionary --- core/addon_preferences.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/core/addon_preferences.py b/core/addon_preferences.py index 1bea83a..31f580d 100644 --- a/core/addon_preferences.py +++ b/core/addon_preferences.py @@ -6,9 +6,14 @@ from ..core.logging_setup import logger from bpy.types import AddonPreferences from typing import Any, Dict -# Get the directory of the current file -PREFERENCES_DIR = os.path.dirname(os.path.abspath(__file__)) -PREFERENCES_FILE = os.path.join(PREFERENCES_DIR, "preferences.json") +# Get the user preferences directory instead of addon directory +def get_preferences_path(): + user_path = bpy.utils.resource_path('USER') + addon_prefs_dir = os.path.join(user_path, "config", "avatar_toolkit_prefs") + os.makedirs(addon_prefs_dir, exist_ok=True) + return os.path.join(addon_prefs_dir, "preferences.json") + +PREFERENCES_FILE = get_preferences_path() def get_current_version(): main_dir = os.path.dirname(os.path.dirname(__file__)) @@ -60,4 +65,4 @@ if not os.path.exists(PREFERENCES_FILE): 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 - save_preference("highlight_problem_bones", True) # Set default bone highlighting \ No newline at end of file + save_preference("highlight_problem_bones", True) # Set default bone highlighting From af5b79e314aa6ee2c695ccdf4e7829417a699da8 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 13:05:50 +0100 Subject: [PATCH 07/20] Removed extra registrations in properties This was a hot fix for some issues we were having, however this is no longer needed as my fix to auto loader fixed the issues which meant we were trying to double regisiter these things. --- core/properties.py | 52 ++-------------------------------------------- 1 file changed, 2 insertions(+), 50 deletions(-) diff --git a/core/properties.py b/core/properties.py index e7886d0..898a6dc 100644 --- a/core/properties.py +++ b/core/properties.py @@ -592,46 +592,7 @@ def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") - # Clear any existing registrations to prevent conflicts - if hasattr(bpy.types.Scene, "avatar_toolkit"): - try: - del bpy.types.Scene.avatar_toolkit - except: - logger.warning("Failed to remove existing avatar_toolkit property") - - # Register classes - try: - # Try to register all classes at once - bpy.utils.register_class(ZeroWeightBoneItem) - bpy.utils.register_class(ValidationMessageItem) - bpy.utils.register_class(AvatarToolkitSceneProperties) - except ValueError as e: - logger.warning(f"Class registration issue: {e}") - # Try to unregister first in case they're already registered - try: - # Try to unregister in reverse order - try: - bpy.utils.unregister_class(AvatarToolkitSceneProperties) - except: - pass - try: - bpy.utils.unregister_class(ValidationMessageItem) - except: - pass - try: - bpy.utils.unregister_class(ZeroWeightBoneItem) - except: - pass - - # Then register again - bpy.utils.register_class(ZeroWeightBoneItem) - bpy.utils.register_class(ValidationMessageItem) - bpy.utils.register_class(AvatarToolkitSceneProperties) - except Exception as e: - logger.error(f"Failed to recover from registration error: {e}") - raise - - # Register the property + # Only register the property, not the classes (auto_load will handle that) bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties) logger.debug("Properties registered successfully") @@ -640,20 +601,11 @@ def unregister() -> None: """Unregister the Avatar Toolkit property group""" logger.info("Unregistering Avatar Toolkit properties") - # Remove the property first + # Remove the property if hasattr(bpy.types.Scene, "avatar_toolkit"): try: del bpy.types.Scene.avatar_toolkit logger.debug("Removed avatar_toolkit property") except Exception as e: logger.warning(f"Failed to remove avatar_toolkit property: {e}") - - # Then unregister the classes - try: - bpy.utils.unregister_class(AvatarToolkitSceneProperties) - bpy.utils.unregister_class(ValidationMessageItem) - bpy.utils.unregister_class(ZeroWeightBoneItem) - logger.debug("Unregistered property classes") - except (RuntimeError, ValueError) as e: - logger.warning(f"Error during property class unregistration: {e}") # Not fatal - continue From c0943e0d20f4da207ddaaddd0ba52cae7f4341cc Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 13:48:52 +0100 Subject: [PATCH 08/20] Atlas Materials UI Update - Separated things out so it looks better. - Done small changes to make the ui look a bit better. - UI auto refreshes after a successful atlas. --- functions/atlas_materials.py | 11 ++ resources/translations/en_US.json | 17 +++ resources/translations/ja_JP.json | 17 +++ resources/translations/ko_KR.json | 17 +++ ui/atlas_materials_panel.py | 185 ++++++++++++++++++++++-------- 5 files changed, 202 insertions(+), 45 deletions(-) diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py index ee807cf..74487fa 100644 --- a/functions/atlas_materials.py +++ b/functions/atlas_materials.py @@ -280,6 +280,17 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): mesh.materials[i] = atlased_mat.material progress.step(f"Updated materials for {obj.name}") + MaterialListBool.old_list.pop(context.scene.name, None) + was_open = context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown + context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False + + if was_open: + bpy.ops.avatar_toolkit.expand_section_materials() + + for area in context.screen.areas: + if area.type == 'VIEW_3D': + area.tag_redraw() + logger.info("Material atlas creation completed successfully") self.report({'INFO'}, t("TextureAtlas.atlas_completed")) return {"FINISHED"} diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index ad15fd8..eaf9ed0 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -472,6 +472,23 @@ "TextureAtlas.ambient_occlusion": "Ambient Occlusion", "TextureAtlas.height": "Height", "TextureAtlas.roughness": "Roughness", + "TextureAtlas.description_1": "Create a single material with combined textures", + "TextureAtlas.description_2": "to optimize your avatar for better performance.", + "TextureAtlas.texture_maps": "Texture Maps", + "TextureAtlas.material_ready": "Material is ready for atlas", + "TextureAtlas.material_not_ready": "Material needs at least one texture", + "TextureAtlas.select_all_tooltip": "Select all materials for atlas", + "TextureAtlas.select_none_tooltip": "Deselect all materials", + "TextureAtlas.expand_all_tooltip": "Expand all material settings", + "TextureAtlas.collapse_all_tooltip": "Collapse all material settings", + "TextureAtlas.estimated_size": "Estimated Atlas Size", + "TextureAtlas.materials": "materials", + "TextureAtlas.no_materials_selected": "No materials selected", + "TextureAtlas.select_armature_first": "Please select an armature first", + "TextureAtlas.how_to_use_1": "1. Select an armature in the scene", + "TextureAtlas.how_to_use_2": "2. Click 'Load Materials' to begin", + "TextureAtlas.load_error": "Error loading materials. Check console for details.", + "TextureAtlas.material_not_included": "Material is not included in atlas", "Settings.label": "Settings", "Settings.language": "Language", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 18b23d9..677b892 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -472,6 +472,23 @@ "TextureAtlas.ambient_occlusion": "アンビエントオクルージョン", "TextureAtlas.height": "高さ", "TextureAtlas.roughness": "粗さ", + "TextureAtlas.description_1": "テクスチャを組み合わせた単一のマテリアルを作成", + "TextureAtlas.description_2": "アバターのパフォーマンスを最適化します。", + "TextureAtlas.texture_maps": "テクスチャマップ", + "TextureAtlas.material_ready": "マテリアルはアトラス作成の準備ができています", + "TextureAtlas.material_not_ready": "マテリアルには少なくとも1つのテクスチャが必要です", + "TextureAtlas.select_all_tooltip": "すべてのマテリアルを選択", + "TextureAtlas.select_none_tooltip": "すべての選択を解除", + "TextureAtlas.expand_all_tooltip": "すべてのマテリアル設定を展開", + "TextureAtlas.collapse_all_tooltip": "すべてのマテリアル設定を折りたたむ", + "TextureAtlas.estimated_size": "推定アトラスサイズ", + "TextureAtlas.materials": "マテリアル", + "TextureAtlas.no_materials_selected": "マテリアルが選択されていません", + "TextureAtlas.select_armature_first": "最初にアーマチュアを選択してください", + "TextureAtlas.how_to_use_1": "1. シーン内のアーマチュアを選択", + "TextureAtlas.how_to_use_2": "2. 「マテリアルを読み込む」をクリックして開始", + "TextureAtlas.load_error": "マテリアルの読み込みエラー。詳細はコンソールを確認してください。", + "TextureAtlas.material_not_included": "マテリアルはアトラスに含まれていません", "Settings.label": "設定", "Settings.language": "言語", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index 4fdaa97..06cb68d 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -472,6 +472,23 @@ "TextureAtlas.ambient_occlusion": "앰비언트 오클루전", "TextureAtlas.height": "높이", "TextureAtlas.roughness": "거칠기", + "TextureAtlas.description_1": "텍스처를 결합한 단일 재질 생성", + "TextureAtlas.description_2": "아바타의 성능을 최적화합니다.", + "TextureAtlas.texture_maps": "텍스처 맵", + "TextureAtlas.material_ready": "재질이 아틀라스 생성 준비가 되었습니다", + "TextureAtlas.material_not_ready": "재질에는 최소 하나의 텍스처가 필요합니다", + "TextureAtlas.select_all_tooltip": "모든 재질 선택", + "TextureAtlas.select_none_tooltip": "모든 선택 해제", + "TextureAtlas.expand_all_tooltip": "모든 재질 설정 펼치기", + "TextureAtlas.collapse_all_tooltip": "모든 재질 설정 접기", + "TextureAtlas.estimated_size": "예상 아틀라스 크기", + "TextureAtlas.materials": "재질", + "TextureAtlas.no_materials_selected": "선택된 재질이 없습니다", + "TextureAtlas.select_armature_first": "먼저 아마추어를 선택해주세요", + "TextureAtlas.how_to_use_1": "1. 장면에서 아마추어 선택", + "TextureAtlas.how_to_use_2": "2. '재질 불러오기'를 클릭하여 시작", + "TextureAtlas.load_error": "재질 로딩 오류. 자세한 내용은 콘솔을 확인하세요.", + "TextureAtlas.material_not_included": "재질이 아틀라스에 포함되지 않았습니다", "Settings.label": "설정", "Settings.language": "언어", diff --git a/ui/atlas_materials_panel.py b/ui/atlas_materials_panel.py index 2d9b70c..f7edfd8 100644 --- a/ui/atlas_materials_panel.py +++ b/ui/atlas_materials_panel.py @@ -5,6 +5,7 @@ from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME from ..core.common import SceneMatClass, MaterialListBool, get_active_armature from ..functions.atlas_materials import AvatarToolKit_OT_AtlasMaterials from ..core.translations import t +from ..core.logging_setup import logger class AvatarToolKit_OT_SelectAllMaterials(Operator): bl_idname = 'avatar_toolkit.select_all_materials' @@ -56,22 +57,33 @@ class AvatarToolKit_OT_ExpandSectionMaterials(Operator): return True def execute(self, context: Context) -> set: - if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown: - context.scene.avatar_toolkit.materials.clear() - newlist: list[Material] = [] - for obj in 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.avatar_toolkit.materials.add() - newitem.mat = mat_slot.material - MaterialListBool.old_list[context.scene.name] = newlist - context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = True - else: - context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False - return {'FINISHED'} + try: + if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown: + context.scene.avatar_toolkit.materials.clear() + newlist: list[Material] = [] + + logger.debug("Loading materials for texture atlas") + for obj in 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.avatar_toolkit.materials.add() + newitem.mat = mat_slot.material + + MaterialListBool.old_list[context.scene.name] = newlist + context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = True + logger.info(f"Loaded {len(newlist)} materials for texture atlas") + else: + context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown = False + logger.debug("Hiding material list") + + return {'FINISHED'} + except Exception as e: + logger.error(f"Error loading materials: {str(e)}", exc_info=True) + self.report({'ERROR'}, t("TextureAtlas.load_error")) + return {'CANCELLED'} class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList): bl_label = t("TextureAtlas.material_list_label") @@ -81,17 +93,30 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList): 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.avatar_toolkit, "material_search_filter", text="", icon='VIEWZOOM') + row = layout.row(align=True) + row.scale_y = 1.2 + + row.operator("avatar_toolkit.select_all_materials", text="", icon='CHECKBOX_HLT', + emboss=True).tooltip = t("TextureAtlas.select_all_tooltip") + row.operator("avatar_toolkit.select_none_materials", text="", icon='CHECKBOX_DEHLT', + emboss=True).tooltip = t("TextureAtlas.select_none_tooltip") + row.separator(factor=0.5) + row.operator("avatar_toolkit.expand_all_materials", text="", icon='DISCLOSURE_TRI_DOWN', + emboss=True).tooltip = t("TextureAtlas.expand_all_tooltip") + row.operator("avatar_toolkit.collapse_all_materials", text="", icon='DISCLOSURE_TRI_RIGHT', + emboss=True).tooltip = t("TextureAtlas.collapse_all_tooltip") + + row.separator(factor=1.0) + search_row = row.row() + search_row.scale_x = 2.0 + search_row.prop(context.scene.avatar_toolkit, "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") + size_row = box.row() + size_row.alignment = 'CENTER' + size_text = self.calculate_atlas_size(context) + size_row.label(text=f"{t('TextureAtlas.estimated_size')}: {size_text}px", icon='TEXTURE') def draw_item(self, context: Context, layout: UILayout, data: Object, item: SceneMatClass, icon, active_data, active_propname, index): if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown: @@ -99,34 +124,64 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList): context.scene.avatar_toolkit.material_search_filter.lower() not in item.mat.name.lower()): return + # Main material row = layout.row() + row.prop(item.mat, "include_in_atlas", text="", + icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT', + emboss=False) - row.prop(item.mat, "include_in_atlas", text="", icon='CHECKBOX_HLT' if item.mat.include_in_atlas else 'CHECKBOX_DEHLT') - + # Material name row.prop(item.mat, "material_expanded", text=item.mat.name, icon='DOWNARROW_HLT' if item.mat.material_expanded else 'RIGHTARROW', emboss=False) - if item.mat.material_expanded and item.mat.include_in_atlas: + row.label(text="", icon='MATERIAL') + + if item.mat.material_expanded: 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") + + header_row = col.row() + header_row.alignment = 'CENTER' + header_row.label(text=t("TextureAtlas.texture_maps"), icon='IMAGE') + col.separator(factor=0.5) + self.draw_texture_row(col, item.mat, "texture_atlas_albedo", "IMAGE_RGB", t("TextureAtlas.albedo")) + self.draw_texture_row(col, item.mat, "texture_atlas_normal", "NORMALS_FACE", t("TextureAtlas.normal")) + self.draw_texture_row(col, item.mat, "texture_atlas_emission", "LIGHT", t("TextureAtlas.emission")) + self.draw_texture_row(col, item.mat, "texture_atlas_ambient_occlusion", "SHADING_SOLID", t("TextureAtlas.ambient_occlusion")) + self.draw_texture_row(col, item.mat, "texture_atlas_height", "IMAGE_ZDEPTH", t("TextureAtlas.height")) + self.draw_texture_row(col, item.mat, "texture_atlas_roughness", "MATERIAL", t("TextureAtlas.roughness")) col.separator(factor=0.5) + + status_row = col.row() + status_row.alignment = 'CENTER' + is_ready = self.is_material_ready(item.mat) + + if item.mat.include_in_atlas: + status_text = t("TextureAtlas.material_ready") if is_ready else t("TextureAtlas.material_not_ready") + status_icon = 'CHECKMARK' if is_ready else 'ERROR' + else: + status_text = t("TextureAtlas.material_not_included") + status_icon = 'INFO' + + status_row.label(text=status_text, icon=status_icon) - def draw_texture_row(self, layout, material, prop_name, icon): - row = layout.row() - row.prop(material, prop_name, icon=icon) + def draw_texture_row(self, layout, material, prop_name, icon, label_text): + row = layout.row(align=True) + icon_row = row.row() + icon_row.scale_x = 0.5 + icon_row.label(text="", icon=icon) + + # Texture selector + row.prop(material, prop_name, text=label_text) + status_row = row.row() + status_row.scale_x = 0.5 if getattr(material, prop_name): - row.label(text="", icon='CHECKMARK') + status_row.label(text="", icon='CHECKMARK') else: - row.label(text="", icon='X') + status_row.label(text="", icon='X') def is_material_ready(self, material): return bool(material.texture_atlas_albedo or @@ -135,12 +190,21 @@ class AvatarToolKit_UL_MaterialTextureAtlasProperties(UIList): def calculate_atlas_size(self, context): total_size = 0 + selected_count = 0 + for mat in context.scene.avatar_toolkit.materials: if mat.mat.include_in_atlas: + selected_count += 1 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))}" + + if total_size == 0: + return f"0x0 ({t('TextureAtlas.no_materials_selected')})" + size = int(sqrt(total_size)) + pot_size = 2 ** (size - 1).bit_length() # Next power of 2 + + return f"{pot_size}x{pot_size} ({selected_count} {t('TextureAtlas.materials')})" class AvatarToolKit_PT_TextureAtlasPanel(Panel): bl_label = t("TextureAtlas.label") @@ -156,16 +220,26 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): armature = get_active_armature(context) if armature: - layout.label(text=t("TextureAtlas.label"), icon='TEXTURE') + header_row = layout.row() + header_row.label(text=t("TextureAtlas.label"), icon='TEXTURE') layout.separator(factor=0.5) + info_box = layout.box() + info_col = info_box.column() + info_col.scale_y = 0.9 + info_col.label(text=t("TextureAtlas.description_1"), icon='INFO') + info_col.label(text=t("TextureAtlas.description_2")) + layout.separator(factor=0.5) box = layout.box() - row = box.row() + row = box.row(align=True) + row.scale_y = 1.2 direction_icon = 'RIGHTARROW' if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else 'DOWNARROW_HLT' + button_text = t("TextureAtlas.reload_list") if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list") row.operator(AvatarToolKit_OT_ExpandSectionMaterials.bl_idname, - text=(t("TextureAtlas.reload_list") if not context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown else t("TextureAtlas.loaded_list")), + text=button_text, icon=direction_icon) + # Material list expanded if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown: row = box.row() row.template_list(AvatarToolKit_UL_MaterialTextureAtlasProperties.bl_idname, @@ -181,8 +255,29 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): row = layout.row() row.scale_y = 1.5 - row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname, - text=t("TextureAtlas.atlas_materials"), - icon='NODE_TEXTURE') + row.enabled = context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown + + has_selected = False + if context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown: + for item in context.scene.avatar_toolkit.materials: + if item.mat.include_in_atlas: + has_selected = True + break + + if not has_selected and context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown: + row.operator(AvatarToolKit_OT_AtlasMaterials.bl_idname, + text=t("TextureAtlas.no_materials_selected"), + icon='ERROR') + else: + 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') + + box = layout.box() + col = box.column() + col.scale_y = 0.9 + col.label(text=t("TextureAtlas.select_armature_first"), icon='INFO') + col.label(text=t("TextureAtlas.how_to_use_1")) + col.label(text=t("TextureAtlas.how_to_use_2")) From 1e8784d0e43c5aa1ec11cfe842f107071be27007 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 13:49:44 +0100 Subject: [PATCH 09/20] Alpha 2 0.2.1 --- blender_manifest.toml | 2 +- resources/translations/en_US.json | 2 +- resources/translations/ja_JP.json | 2 +- resources/translations/ko_KR.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/blender_manifest.toml b/blender_manifest.toml index cd05857..068cd93 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -3,7 +3,7 @@ schema_version = "1.0.0" id = "avatar_toolkit" -version = "0.2.0" +version = "0.2.1" name = "Avatar Toolkit" tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." maintainer = "Team NekoNeo" diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index eaf9ed0..d56bf50 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.0)", + "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.2.1)", "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.", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 677b892..343271b 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "アバターツールキット (アルファ 0.2.0)", + "AvatarToolkit.label": "アバターツールキット (アルファ 0.2.1)", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc3": "GitHubで報告してください。", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index 06cb68d..6fca745 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "아바타 툴킷 (알파 0.2.0)", + "AvatarToolkit.label": "아바타 툴킷 (알파 0.2.1)", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc3": "Github에 보고해 주세요.", From 64a78dbbb2cf8a9b0b485cb14de42a1ed6a1293d Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 13:57:12 +0100 Subject: [PATCH 10/20] Remove the annoying logger about non standard bones --- core/armature_validation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/core/armature_validation.py b/core/armature_validation.py index 446bac4..ad1212b 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -113,7 +113,6 @@ def validate_armature(armature: Object, detailed_messages: bool = False) -> Unio non_standard_bones.append(bone_name) if non_standard_bones: - logger.warning(f"Found {len(non_standard_bones)} non-standard bones") non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones]) non_standard_messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list)) From 345ba444638dccf4d8cff215583d3e690200e777 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 22:39:49 +0100 Subject: [PATCH 11/20] Texture Atlas should require the user to save the blender files before we allow them to atlas --- functions/atlas_materials.py | 14 ++++++++++++-- resources/translations/en_US.json | 4 ++++ resources/translations/ja_JP.json | 4 ++++ resources/translations/ko_KR.json | 4 ++++ ui/atlas_materials_panel.py | 10 ++++++++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py index 74487fa..b578fcc 100644 --- a/functions/atlas_materials.py +++ b/functions/atlas_materials.py @@ -137,6 +137,10 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): @classmethod def poll(cls, context: Context) -> bool: + # Only allow operation if the file is saved and materials are selected. + if not bpy.data.filepath: + cls.poll_message_set(t("TextureAtlas.save_file_first")) + return False return context.scene.avatar_toolkit.texture_atlas_Has_Mat_List_Shown def execute(self, context: Context) -> set: @@ -208,8 +212,14 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): image_pixels[int(((k*w)+i)*4)+channel] canvas.pixels[:] = canvas_pixels[:] - canvas.save(filepath=os.path.join(os.path.dirname(bpy.data.filepath), - new_image_name+".png")) + + try: + save_dir = os.path.dirname(bpy.data.filepath) + canvas.save(filepath=os.path.join(save_dir, new_image_name+".png")) + except Exception as save_error: + logger.error(f"Failed to save atlas texture: {str(save_error)}") + self.report({'WARNING'}, f"Could not save texture to disk. Atlas will work in memory only.") + setattr(atlased_mat, type_name, canvas) progress.step(f"Created {type_name} atlas") diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index d56bf50..39b1183 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -489,6 +489,10 @@ "TextureAtlas.how_to_use_2": "2. Click 'Load Materials' to begin", "TextureAtlas.load_error": "Error loading materials. Check console for details.", "TextureAtlas.material_not_included": "Material is not included in atlas", + "TextureAtlas.save_file_first": "Please save your Blender file before creating a texture atlas", + "TextureAtlas.save_file_instructions": "Use File > Save As... or click the button below:", + "TextureAtlas.save_file_button": "Save Blender File", + "TextureAtlas.save_file_required": "Save File Required", "Settings.label": "Settings", "Settings.language": "Language", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 343271b..22282b0 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -489,6 +489,10 @@ "TextureAtlas.how_to_use_2": "2. 「マテリアルを読み込む」をクリックして開始", "TextureAtlas.load_error": "マテリアルの読み込みエラー。詳細はコンソールを確認してください。", "TextureAtlas.material_not_included": "マテリアルはアトラスに含まれていません", + "TextureAtlas.save_file_first": "テクスチャアトラスを作成する前に、Blenderファイルを保存してください", + "TextureAtlas.save_file_instructions": "ファイル > 名前を付けて保存... を使用するか、下のボタンをクリックしてください:", + "TextureAtlas.save_file_button": "Blenderファイルを保存", + "TextureAtlas.save_file_required": "ファイルの保存が必要です", "Settings.label": "設定", "Settings.language": "言語", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index 6fca745..f80a09d 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -489,6 +489,10 @@ "TextureAtlas.how_to_use_2": "2. '재질 불러오기'를 클릭하여 시작", "TextureAtlas.load_error": "재질 로딩 오류. 자세한 내용은 콘솔을 확인하세요.", "TextureAtlas.material_not_included": "재질이 아틀라스에 포함되지 않았습니다", + "TextureAtlas.save_file_first": "텍스처 아틀라스를 만들기 전에 Blender 파일을 저장하세요", + "TextureAtlas.save_file_instructions": "파일 > 다른 이름으로 저장... 을 사용하거나 아래 버튼을 클릭하세요:", + "TextureAtlas.save_file_button": "Blender 파일 저장", + "TextureAtlas.save_file_required": "파일 저장 필요", "Settings.label": "설정", "Settings.language": "언어", diff --git a/ui/atlas_materials_panel.py b/ui/atlas_materials_panel.py index f7edfd8..ee940ba 100644 --- a/ui/atlas_materials_panel.py +++ b/ui/atlas_materials_panel.py @@ -229,6 +229,16 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): info_col.label(text=t("TextureAtlas.description_1"), icon='INFO') info_col.label(text=t("TextureAtlas.description_2")) + if not bpy.data.filepath: + warning_box = layout.box() + warning_col = warning_box.column() + warning_col.scale_y = 0.9 + warning_col.alert = True + warning_col.label(text=t("TextureAtlas.save_file_first"), icon='ERROR') + warning_col.label(text=t("TextureAtlas.save_file_instructions")) + warning_col.operator("wm.save_as_mainfile", text=t("TextureAtlas.save_file_button"), icon='FILE_TICK') + layout.separator(factor=0.5) + layout.separator(factor=0.5) box = layout.box() row = box.row(align=True) From ff5efc9639210b8ff21c5e4f43fe673be1958817 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 22:51:35 +0100 Subject: [PATCH 12/20] Update blender_manifest.toml --- blender_manifest.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/blender_manifest.toml b/blender_manifest.toml index 068cd93..97812f3 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -15,6 +15,10 @@ license = [ "SPDX:GPL-3.0-or-later", ] +[permissions] +network = "For the auto updater to work, you need to allow network access." +files = "Import/ Export files, saving atlas images, saving preferences." + wheels = [ "./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl", "./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl", From a407e99ebd9f21317b9d3774adee39babb104b9a Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 23:03:09 +0100 Subject: [PATCH 13/20] Update atlas_materials.py --- functions/atlas_materials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/functions/atlas_materials.py b/functions/atlas_materials.py index b578fcc..8c38f9e 100644 --- a/functions/atlas_materials.py +++ b/functions/atlas_materials.py @@ -218,7 +218,7 @@ class AvatarToolKit_OT_AtlasMaterials(Operator): canvas.save(filepath=os.path.join(save_dir, new_image_name+".png")) except Exception as save_error: logger.error(f"Failed to save atlas texture: {str(save_error)}") - self.report({'WARNING'}, f"Could not save texture to disk. Atlas will work in memory only.") + self.report({'WARNING'}, f"Could not save texture to disk, This may be due to a lack of permissions.") setattr(atlased_mat, type_name, canvas) progress.step(f"Created {type_name} atlas") From 0ff2dc1c3826aa3b036ec45c2a7db9d47bf3679e Mon Sep 17 00:00:00 2001 From: Yusarina Date: Mon, 31 Mar 2025 23:22:35 +0100 Subject: [PATCH 14/20] I swear I fixed the issue before we Armautre and Mesh attach, also fixed permission. --- blender_manifest.toml | 8 ++++---- ui/custom_avatar_panel.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/blender_manifest.toml b/blender_manifest.toml index 97812f3..b6e9679 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -15,13 +15,13 @@ license = [ "SPDX:GPL-3.0-or-later", ] -[permissions] -network = "For the auto updater to work, you need to allow network access." -files = "Import/ Export files, saving atlas images, saving preferences." - wheels = [ "./wheels/lz4-4.4.3-cp311-cp311-macosx_11_0_arm64.whl", "./wheels/lz4-4.3.3-cp311-cp311-macosx_10_9_x86_64.whl", "./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", "./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl" ] + +[permissions] +network = "For the auto updater to work, you need to allow network access" +files = "Import/Export files, saving atlas images, saving preferences" diff --git a/ui/custom_avatar_panel.py b/ui/custom_avatar_panel.py index 4e3ff62..3ebf160 100644 --- a/ui/custom_avatar_panel.py +++ b/ui/custom_avatar_panel.py @@ -2,6 +2,8 @@ import bpy from typing import Set, List, Tuple, Any from bpy.types import Panel, Context, UILayout, Operator, Event, WindowManager from .main_panel import AvatarToolKit_PT_AvatarToolkitPanel, CATEGORY_NAME +from ..functions.custom_tools.mesh_attachment import AvatarToolkit_OT_AttachMesh +from ..functions.custom_tools.armature_merging import AvatarToolkit_OT_MergeArmature from ..core.translations import t from ..core.common import ( get_active_armature, From feb2f5ac85db08d83ca918bc621dba7051ed79d4 Mon Sep 17 00:00:00 2001 From: 989onan Date: Mon, 31 Mar 2025 18:28:04 -0400 Subject: [PATCH 15/20] Create centralized method for identifying bones - also fixes an issue where VRM bones would never be identified due to the names having "_" in them. --- core/common.py | 14 +++++++++++++ core/dictionaries.py | 46 +++++++++++++++++++++++++----------------- core/resonite_utils.py | 28 +++++++++---------------- 3 files changed, 52 insertions(+), 36 deletions(-) diff --git a/core/common.py b/core/common.py index c232856..83a8ff3 100644 --- a/core/common.py +++ b/core/common.py @@ -18,6 +18,7 @@ from bpy.utils import register_class from ..core.logging_setup import logger from ..core.translations import t from ..core.dictionaries import bone_names +from .dictionaries import reverse_bone_lookup, bone_names class ProgressTracker: """Universal progress tracking for Avatar Toolkit operations""" @@ -412,6 +413,19 @@ def simplify_bonename(name: str) -> str: """Simplify bone name by removing spaces, underscores, dots and converting to lowercase""" return name.lower().translate(dict.fromkeys(map(ord, u" _."))) + +def identify_bones(arm_data: bpy.types.Armature, context: bpy.types.Context) -> Dict[str,str]: + """Identify bone names in an armature based on our reverse dictionary, so there is no confusion to what a bone is. + Essentially makes a dictionary of keys from dictionaries.bone_names like "hips", and the corosponding value is the bone that can be mapped to that key.""" + returned: Dict[str,str] = {} + for bone in arm_data.bones: + + simplified_name = simplify_bonename(bone.name) + + if simplified_name in reverse_bone_lookup: + returned[reverse_bone_lookup[simplified_name]] = bone.name + return returned + def duplicate_bone_chain(bones: List[EditBone]) -> List[EditBone]: """Duplicate a chain of bones while preserving hierarchy""" new_bones: List[EditBone] = [] diff --git a/core/dictionaries.py b/core/dictionaries.py index 9a54b05..ec594a9 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -1,8 +1,8 @@ # GPL Licence - # Bone names from https://github.com/triazo/immersive_scaler/ -# 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... +# Note from @989onan: Please make sure to make your names are lowercase in this array, or it will never find a match. I banged my head metaphorically till I figured that out... +# Note2: Remove all "_", ".", and " " (space) from your values array or it will also not ever find a match!!!! # Taken from Tuxedo/Cats bone_names = { # Right side bones @@ -254,26 +254,26 @@ bone_names = { # 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'], + 'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'], + 'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'], + 'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest'], + 'upper_chest': bone_names['upperchest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'], + 'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'], + 'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'], # 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'], + 'thumb_0_l': bone_names['thumb_0_l'] + ['thumbmetacarpall', 'jthumb1l'], + 'index_0_l': bone_names['index_0_l'] + ['indexmetacarpall', 'jindex1l'], + 'middle_0_l': bone_names['middle_0_l'] + ['middlemetacarpall', 'jmiddle1l'], + 'ring_0_l': bone_names['ring_0_l'] + ['ringmetacarpall', 'jring1l'], + 'pinkie_0_l': bone_names['pinkie_0_l'] + ['littlemetacarpall', 'jlittle1l'], # 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'] + 'thumb_0_r': bone_names['thumb_0_r'] + ['thumbmetacarpalr', 'jthumb1r'], + 'index_0_r': bone_names['index_0_r'] + ['indexmetacarpalr', 'jindex1r'], + 'middle_0_r': bone_names['middle_0_r'] + ['middlemetacarpalr', 'jmiddle1r'], + 'ring_0_r': bone_names['ring_0_r'] + ['ringmetacarpalr', 'jring1r'], + 'pinkie_0_r': bone_names['pinkie_0_r'] + ['littlemetacarpalr', 'jlittle1r'] }) # array taken from cats @@ -354,3 +354,13 @@ resonite_translations = { 'thumb_2_r': "thumb2.R", 'thumb_3_r': "thumb3.R" } + + +# Create reverse lookup dictionary (conversion/translation) +reverse_bone_lookup = {} +for preferred_name, name_list in bone_names.items(): + for name in name_list: + reverse_bone_lookup[name] = preferred_name + + + diff --git a/core/resonite_utils.py b/core/resonite_utils.py index f6938d2..c1e459c 100644 --- a/core/resonite_utils.py +++ b/core/resonite_utils.py @@ -3,14 +3,15 @@ import bpy import bpy_extras from numpy import double from typing import Set, Dict +import re -from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker +from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker, identify_bones from bpy.types import Context, Operator from ..core.translations import t from ..core.dictionaries import bone_names, resonite_translations from ..core.logging_setup import logger -import re + from .resonite_loader import resonite_animx, resonite_types import os @@ -64,30 +65,21 @@ class AvatarToolkit_OT_ConvertResonite(Operator): 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') - + arm_data: bpy.types.Armature = armature.data # Cache simplified bone names - for bone in armature.data.bones: - simplified_names[bone.name] = simplify_bonename(bone.name) + for bone in arm_data.bones: + bone.name = re.compile(re.escape(""), re.IGNORECASE).sub("", bone.name) - total_bones = len(armature.data.bones) + total_bones = len(arm_data.bones) with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress: - for bone in armature.data.bones: - # Remove any existing "" tags - bone.name = re.compile(re.escape(""), re.IGNORECASE).sub("", bone.name) - simplified_name = simplified_names[bone.name] + for key_simple,bone_name in identify_bones(arm_data,context).items(): - 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]] + if key_simple in resonite_translations: + new_name = resonite_translations[key_simple] logger.debug(f"Translating bone: {bone.name} -> {new_name}") bone.name = new_name else: From eba18d72a68cc21da3fb6d25dd834fade7abd4ad Mon Sep 17 00:00:00 2001 From: 989onan Date: Mon, 31 Mar 2025 18:45:30 -0400 Subject: [PATCH 16/20] Fix poisoned name sets this is a simple patch because policing the standard is literally impossible --- core/dictionaries.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/core/dictionaries.py b/core/dictionaries.py index 5769260..c5525ec 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -4,6 +4,9 @@ # Note from @989onan: Please make sure to make your names are lowercase in this array, or it will never find a match. I banged my head metaphorically till I figured that out... # Note2: Remove all "_", ".", and " " (space) from your values array or it will also not ever find a match!!!! # Taken from Tuxedo/Cats + +from .common import simplify_bonename + bone_names = { # Right side bones "right_shoulder": [ @@ -354,11 +357,6 @@ resonite_translations = { 'thumb_2_r': "thumb2.R", 'thumb_3_r': "thumb3.R" } -# Create reverse lookup dictionary (conversion/translation) -reverse_bone_lookup = {} -for preferred_name, name_list in bone_names.items(): - for name in name_list: - reverse_bone_lookup[name] = preferred_name @@ -948,3 +946,15 @@ for category, mappings in non_standard_mappings.items(): bone_names[category].extend(mappings) else: bone_names[category] = mappings + + +# Since data set is very poisoned by bone names that aren't simplified (And as such will not map properly using the function) we will just force convert them to the proper format at the end here. - @989onan +for standard, mappings in bone_names: + for i in len(mappings): + bone_names[standard][i] = simplify_bonename(mappings[i]) + +# Create reverse lookup dictionary (conversion/translation) +reverse_bone_lookup = {} +for preferred_name, name_list in bone_names.items(): + for name in name_list: + reverse_bone_lookup[name] = preferred_name From 1ca45ad9016178f032ada0377b98b0f5c548bf25 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 1 Apr 2025 00:45:56 +0100 Subject: [PATCH 17/20] Move simpify_bonename to dictionaries --- core/common.py | 5 ----- core/dictionaries.py | 4 +++- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/core/common.py b/core/common.py index 62dc51f..4190571 100644 --- a/core/common.py +++ b/core/common.py @@ -383,11 +383,6 @@ def clear_unused_data_blocks() -> int: if isinstance(getattr(bpy.data, attr), bpy.types.bpy_prop_collection)) return initial_count - final_count -def simplify_bonename(name: str) -> str: - """Simplify bone name by removing spaces, underscores, dots and converting to lowercase""" - return name.lower().translate(dict.fromkeys(map(ord, u" _."))) - - def identify_bones(arm_data: bpy.types.Armature, context: bpy.types.Context) -> Dict[str,str]: """Identify bone names in an armature based on our reverse dictionary, so there is no confusion to what a bone is. Essentially makes a dictionary of keys from dictionaries.bone_names like "hips", and the corosponding value is the bone that can be mapped to that key.""" diff --git a/core/dictionaries.py b/core/dictionaries.py index c5525ec..20aefe9 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -5,7 +5,9 @@ # Note2: Remove all "_", ".", and " " (space) from your values array or it will also not ever find a match!!!! # Taken from Tuxedo/Cats -from .common import simplify_bonename +def simplify_bonename(name: str) -> str: + """Simplify bone name by removing spaces, underscores, dots and converting to lowercase""" + return name.lower().translate(dict.fromkeys(map(ord, u" _."))) bone_names = { # Right side bones From 5a43a9d66d341d127fb270687b3a1e9fce0c933b Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 1 Apr 2025 00:56:56 +0100 Subject: [PATCH 18/20] Fix two different error - bones_names is a dictionary so bone_names.items should be used - also len(mappings) was being used incorrectly should be using range(len(mappings)) - The last one is it just didn't like upperchest in bone_names.update so as a very temp solution i used upper_chest no an ideal solution but I too tired to investogate today and just wanted the plugin to work. however it should be simpilify anyone for now. --- core/dictionaries.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/dictionaries.py b/core/dictionaries.py index 20aefe9..8e11fdf 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -262,7 +262,7 @@ bone_names.update({ 'hips': bone_names['hips'] + ['jbipchips', 'jhips', 'vrmhips'], 'spine': bone_names['spine'] + ['jbipcspine', 'jspine', 'vrmspine'], 'chest': bone_names['chest'] + ['jbipcchest', 'jchest', 'vrmchest'], - 'upper_chest': bone_names['upperchest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'], + 'upper_chest': bone_names['upper_chest'] + ['jbipcupperchest', 'jupperchest', 'vrmupperchest'], 'neck': bone_names['neck'] + ['jbipcneck', 'jneck', 'vrmneck'], 'head': bone_names['head'] + ['jbipchead', 'jhead', 'vrmhead'], @@ -951,8 +951,8 @@ for category, mappings in non_standard_mappings.items(): # Since data set is very poisoned by bone names that aren't simplified (And as such will not map properly using the function) we will just force convert them to the proper format at the end here. - @989onan -for standard, mappings in bone_names: - for i in len(mappings): +for standard, mappings in bone_names.items(): + for i in range(len(mappings)): bone_names[standard][i] = simplify_bonename(mappings[i]) # Create reverse lookup dictionary (conversion/translation) From 12083c28c532fea0dcc74373fba542ea878367ed Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 1 Apr 2025 17:32:45 +0100 Subject: [PATCH 19/20] Small fixes --- core/updater.py | 2 +- functions/custom_tools/armature_merging.py | 2 +- ui/atlas_materials_panel.py | 2 +- ui/settings_panel.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/core/updater.py b/core/updater.py index bfe4cbc..125cc7a 100644 --- a/core/updater.py +++ b/core/updater.py @@ -84,7 +84,7 @@ 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 = 8 + bl_order = 9 bl_options = {'DEFAULT_CLOSED'} def draw(self, context: bpy.types.Context) -> None: diff --git a/functions/custom_tools/armature_merging.py b/functions/custom_tools/armature_merging.py index cfe197a..133fe65 100644 --- a/functions/custom_tools/armature_merging.py +++ b/functions/custom_tools/armature_merging.py @@ -11,8 +11,8 @@ from ...core.common import ( clear_unused_data_blocks, join_mesh_objects, remove_unused_shapekeys, - simplify_bonename ) +from ...core.dictionaries import simplify_bonename class AvatarToolkit_OT_MergeArmature(bpy.types.Operator): """Operator for merging two armatures together with their associated meshes""" diff --git a/ui/atlas_materials_panel.py b/ui/atlas_materials_panel.py index ee940ba..8f8c056 100644 --- a/ui/atlas_materials_panel.py +++ b/ui/atlas_materials_panel.py @@ -213,7 +213,7 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel): bl_region_type = 'UI' bl_category = CATEGORY_NAME bl_parent_id = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order = 6 + bl_order = 7 def draw(self, context: Context): layout = self.layout diff --git a/ui/settings_panel.py b/ui/settings_panel.py index d0bd2f3..6e7c322 100644 --- a/ui/settings_panel.py +++ b/ui/settings_panel.py @@ -36,7 +36,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel): bl_region_type: str = 'UI' bl_category: str = CATEGORY_NAME bl_parent_id: str = AvatarToolKit_PT_AvatarToolkitPanel.bl_idname - bl_order: int = 7 + bl_order: int = 8 bl_options = {'DEFAULT_CLOSED'} def draw(self, context: Context) -> None: From 2ad5393f060d5b0c56d5c71d15798636583e390f Mon Sep 17 00:00:00 2001 From: Yusarina Date: Tue, 1 Apr 2025 18:09:50 +0100 Subject: [PATCH 20/20] Removed this as in Utils --- functions/tools/convert_resonite.py | 90 ----------------------------- 1 file changed, 90 deletions(-) delete mode 100644 functions/tools/convert_resonite.py diff --git a/functions/tools/convert_resonite.py b/functions/tools/convert_resonite.py deleted file mode 100644 index a41678a..0000000 --- a/functions/tools/convert_resonite.py +++ /dev/null @@ -1,90 +0,0 @@ -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, ProgressTracker -from ...core.dictionaries import bone_names, resonite_translations -from ...core.armature_validation import validate_armature - -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 - valid, _, _ = validate_armature(armature) - return 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 "" tags - bone.name = re.compile(re.escape(""), 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 + "" - 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'} \ No newline at end of file