Compare commits

..

28 Commits

Author SHA1 Message Date
Yusarina 82a7e67d7e Merge pull request #149 from teamneoneko/Current-Dev
Avatar Toolkit 0.2.1
2025-04-02 16:55:30 +01:00
Yusarina 87c40c02d6 Merge pull request #148 from Yusarina/Current-Dev
Small fixes
2025-04-02 16:54:08 +01:00
Yusarina 2ad5393f06 Removed this as in Utils 2025-04-01 18:09:50 +01:00
Yusarina 12083c28c5 Small fixes 2025-04-01 17:32:45 +01:00
Onan Chew c67f30fb97 Merge pull request #147 from Yusarina/Current-Dev
Move simpify_bonename to dictionaries
2025-03-31 20:01:42 -04:00
Yusarina 5a43a9d66d 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.
2025-04-01 00:56:56 +01:00
Yusarina 1ca45ad901 Move simpify_bonename to dictionaries 2025-04-01 00:45:56 +01:00
Yusarina bc034c5308 Merge pull request #146 from 989onan/patch-1
Create centralized method for identifying bones
2025-04-01 00:05:14 +01:00
989onan eba18d72a6 Fix poisoned name sets
this is a simple patch because policing the standard is literally impossible
2025-03-31 18:45:30 -04:00
Onan Chew 416fbe40e7 Merge branch 'Current-Dev' into patch-1 2025-03-31 18:36:53 -04:00
Onan Chew d296d548e8 Merge pull request #145 from Yusarina/Current-Dev
Texture Atlas now requries the user to save before we allow them to atlas.
2025-03-31 18:29:50 -04:00
989onan feb2f5ac85 Create centralized method for identifying bones
- also fixes an issue where VRM bones would never be identified due to the names having "_" in them.
2025-03-31 18:28:04 -04:00
Yusarina 0ff2dc1c38 I swear I fixed the issue before we Armautre and Mesh attach, also fixed permission. 2025-03-31 23:22:35 +01:00
Yusarina a407e99ebd Update atlas_materials.py 2025-03-31 23:03:09 +01:00
Yusarina ff5efc9639 Update blender_manifest.toml 2025-03-31 22:51:35 +01:00
Yusarina 345ba44463 Texture Atlas should require the user to save the blender files before we allow them to atlas 2025-03-31 22:39:49 +01:00
Onan Chew b67d94e89d Merge pull request #143 from Yusarina/Current-Dev
Alpha 2 0.2.1
2025-03-31 13:49:03 -04:00
Yusarina 64a78dbbb2 Remove the annoying logger about non standard bones 2025-03-31 13:57:12 +01:00
Yusarina 1e8784d0e4 Alpha 2 0.2.1 2025-03-31 13:49:44 +01:00
Yusarina c0943e0d20 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.
2025-03-31 13:48:52 +01:00
Yusarina af5b79e314 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.
2025-03-31 13:05:50 +01:00
Yusarina 334f299e0e User Preferences should save in blenders user dictionary not in the Plugins Dictionary 2025-03-31 13:03:37 +01:00
Onan Chew 9a5f13f858 Merge pull request #137 from Yusarina/Current-Dev
Auto load now works with custom blender extensions folders
2025-03-27 18:34:13 -04:00
Yusarina 77b7b429a5 Remove wheel installation
- Blender should handle this for us, we were installing system wide which is bad.
2025-03-27 21:05:59 +00:00
Yusarina 9a0521dad5 Fix because i stupid 2025-03-27 20:44:39 +00:00
Yusarina d7fee2c961 I don't need to add that check duh 2025-03-27 20:42:43 +00:00
Yusarina 357aa1b6d9 Let's not use System path 2025-03-27 20:16:27 +00:00
Yusarina 02c73ccd2a 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.
2025-03-27 18:00:46 +00:00
19 changed files with 391 additions and 328 deletions
+5 -20
View File
@@ -13,38 +13,23 @@ 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):
show_version_error_popup()
return
# Add wheel installation check
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])
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
+5 -1
View File
@@ -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"
@@ -21,3 +21,7 @@ wheels = [
"./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"
+8 -3
View File
@@ -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__))
-1
View File
@@ -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))
+59 -62
View File
@@ -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,14 +30,24 @@ 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}")
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 +55,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
@@ -67,44 +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_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
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)):
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"""
@@ -112,28 +109,37 @@ 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"]
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]:
@@ -164,7 +170,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]:
@@ -172,25 +179,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:
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
+12 -3
View File
@@ -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 SceneMatClass(PropertyGroup):
mat: PointerProperty(type=Material)
@@ -382,9 +383,17 @@ 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."""
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"""
+37 -18
View File
@@ -1,9 +1,14 @@
# 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
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
"right_shoulder": [
@@ -254,26 +259,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['upper_chest'] + ['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
@@ -355,6 +360,8 @@ resonite_translations = {
'thumb_3_r': "thumb3.R"
}
standard_bones = {
# Core Structure
'hips': 'Hips',
@@ -941,3 +948,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.items():
for i in range(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
+2 -50
View File
@@ -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
+11 -19
View File
@@ -3,15 +3,16 @@ import bpy
import bpy_extras
from numpy import double
from typing import Set, Dict
import re
from .common import get_active_armature, simplify_bonename, 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
from ..core.armature_validation import validate_armature
import re
from .resonite_loader import resonite_animx, resonite_types
import os
@@ -65,30 +66,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)
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
for bone in arm_data.bones:
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]]
total_bones = len(arm_data.bones)
with ProgressTracker(context, total_bones, t("Tools.convert_resonite.operation")) as progress:
for key_simple,bone_name in identify_bones(arm_data,context).items():
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:
+1 -1
View File
@@ -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:
+23 -2
View File
@@ -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, This may be due to a lack of permissions.")
setattr(atlased_mat, type_name, canvas)
progress.step(f"Created {type_name} atlas")
@@ -280,6 +290,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"}
+1 -1
View File
@@ -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"""
-90
View File
@@ -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 "<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'}
+22 -1
View File
@@ -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.",
@@ -472,6 +472,27 @@
"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",
"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",
+22 -1
View File
@@ -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で報告してください。",
@@ -472,6 +472,27 @@
"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": "マテリアルはアトラスに含まれていません",
"TextureAtlas.save_file_first": "テクスチャアトラスを作成する前に、Blenderファイルを保存してください",
"TextureAtlas.save_file_instructions": "ファイル > 名前を付けて保存... を使用するか、下のボタンをクリックしてください:",
"TextureAtlas.save_file_button": "Blenderファイルを保存",
"TextureAtlas.save_file_required": "ファイルの保存が必要です",
"Settings.label": "設定",
"Settings.language": "言語",
+22 -1
View File
@@ -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에 보고해 주세요.",
@@ -472,6 +472,27 @@
"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": "재질이 아틀라스에 포함되지 않았습니다",
"TextureAtlas.save_file_first": "텍스처 아틀라스를 만들기 전에 Blender 파일을 저장하세요",
"TextureAtlas.save_file_instructions": "파일 > 다른 이름으로 저장... 을 사용하거나 아래 버튼을 클릭하세요:",
"TextureAtlas.save_file_button": "Blender 파일 저장",
"TextureAtlas.save_file_required": "파일 저장 필요",
"Settings.label": "설정",
"Settings.language": "언어",
+133 -28
View File
@@ -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,9 +57,12 @@ class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
return True
def execute(self, context: Context) -> set:
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:
@@ -67,11 +71,19 @@ class AvatarToolKit_OT_ExpandSectionMaterials(Operator):
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)
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')
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:
row.label(text="", icon='X')
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, 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):
status_row.label(text="", icon='CHECKMARK')
else:
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")
@@ -149,23 +213,43 @@ 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
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"))
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()
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 +265,29 @@ class AvatarToolKit_PT_TextureAtlasPanel(Panel):
row = layout.row()
row.scale_y = 1.5
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"))
+2
View File
@@ -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,
+1 -1
View File
@@ -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: