Compare commits
27 Commits
0.2.0
..
Current-Dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 87c40c02d6 | |||
| 2ad5393f06 | |||
| 12083c28c5 | |||
| c67f30fb97 | |||
| 5a43a9d66d | |||
| 1ca45ad901 | |||
| bc034c5308 | |||
| eba18d72a6 | |||
| 416fbe40e7 | |||
| d296d548e8 | |||
| feb2f5ac85 | |||
| 0ff2dc1c38 | |||
| a407e99ebd | |||
| ff5efc9639 | |||
| 345ba44463 | |||
| b67d94e89d | |||
| 64a78dbbb2 | |||
| 1e8784d0e4 | |||
| c0943e0d20 | |||
| af5b79e314 | |||
| 334f299e0e | |||
| 9a5f13f858 | |||
| 77b7b429a5 | |||
| 9a0521dad5 | |||
| d7fee2c961 | |||
| 357aa1b6d9 | |||
| 02c73ccd2a |
+6
-21
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
save_preference("highlight_problem_bones", True) # Set default bone highlighting
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
+64
-67
@@ -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)):
|
||||
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"""
|
||||
@@ -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:
|
||||
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
|
||||
|
||||
+12
-3
@@ -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"""
|
||||
|
||||
+38
-19
@@ -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',
|
||||
@@ -940,4 +947,16 @@ for category, mappings in non_standard_mappings.items():
|
||||
if category in bone_names:
|
||||
bone_names[category].extend(mappings)
|
||||
else:
|
||||
bone_names[category] = mappings
|
||||
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
@@ -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
|
||||
|
||||
+10
-18
@@ -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)
|
||||
for bone in arm_data.bones:
|
||||
bone.name = re.compile(re.escape("<noik>"), 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 "<noik>" tags
|
||||
bone.name = re.compile(re.escape("<noik>"), 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:
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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'}
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "言語",
|
||||
|
||||
@@ -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": "언어",
|
||||
|
||||
+151
-46
@@ -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")
|
||||
@@ -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.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"))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user