Avatar Toolkit 0.3.2

- Version bumo
- Fixed standardised avatar only work in strict validation mode.
- Fixed Armature merging is using the armature selection in quick access, not the one you selected in Armature Merging for the base.
- Fixed error where if you were not in object mode merge would fail, it now switches to object mode before merge starting.
_ Merge Armature now attempts to auto populate the merge from and to boxes.
- Fixed bug in general mesh tools spamming the console (It was trying to check nothing).
This commit is contained in:
Yusarina
2025-08-09 00:45:53 +01:00
parent 929cadd596
commit 031b78ee7b
10 changed files with 153 additions and 18 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
schema_version = "1.0.0" schema_version = "1.0.0"
id = "avatar_toolkit" id = "avatar_toolkit"
version = "0.3.1" version = "0.3.2"
name = "Avatar Toolkit" name = "Avatar Toolkit"
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games." tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
maintainer = "Team NekoNeo" maintainer = "Team NekoNeo"
+2 -2
View File
@@ -15,12 +15,12 @@ from ..core.dictionaries import (
) )
from ..core.logging_setup import logger from ..core.logging_setup import logger
def validate_armature(armature: Object, detailed_messages: bool = False) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]: def validate_armature(armature: Object, detailed_messages: bool = False, override_mode: Optional[str] = None) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]:
""" """
Validates armature and returns validation results Validates armature and returns validation results
""" """
logger.debug(f"Validating armature: {armature.name if armature else 'None'}") logger.debug(f"Validating armature: {armature.name if armature else 'None'}")
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode validation_mode = override_mode if override_mode else bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = [] messages: List[str] = []
hierarchy_messages: List[str] = [] hierarchy_messages: List[str] = []
non_standard_messages: List[str] = [] non_standard_messages: List[str] = []
+6
View File
@@ -140,6 +140,12 @@ def get_all_meshes(context: Context) -> List[Object]:
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature] return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
return [] return []
def get_meshes_for_armature(armature: Object) -> List[Object]:
"""Get all mesh objects parented to a specific armature"""
if armature and armature.type == 'ARMATURE':
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
return []
def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]: def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
"""Validate mesh object for pose operations""" """Validate mesh object for pose operations"""
if not mesh_obj.data: if not mesh_obj.data:
+89 -2
View File
@@ -67,6 +67,74 @@ def get_mesh_objects(self, context):
return [('NONE', t("Visemes.no_meshes"), '')] return [('NONE', t("Visemes.no_meshes"), '')]
return meshes return meshes
def auto_populate_merge_armatures(context: Context) -> None:
"""Auto-populate merge armature fields when there are 2+ armatures"""
armatures = [obj for obj in bpy.data.objects if obj.type == 'ARMATURE']
if len(armatures) >= 2:
toolkit = context.scene.avatar_toolkit
if not toolkit.merge_armature_into and not toolkit.merge_armature:
toolkit.merge_armature_into = armatures[0].name
toolkit.merge_armature = armatures[1].name
logger.debug(f"Auto-populated merge armatures: {armatures[0].name} <- {armatures[1].name}")
elif toolkit.merge_armature_into and not toolkit.merge_armature:
for armature in armatures:
if armature.name != toolkit.merge_armature_into:
toolkit.merge_armature = armature.name
logger.debug(f"Auto-populated merge_armature: {armature.name}")
break
elif not toolkit.merge_armature_into and toolkit.merge_armature:
for armature in armatures:
if armature.name != toolkit.merge_armature:
toolkit.merge_armature_into = armature.name
logger.debug(f"Auto-populated merge_armature_into: {armature.name}")
break
def update_merge_armature_into(self: PropertyGroup, context: Context) -> None:
"""Update function for merge_armature_into property"""
auto_populate_merge_armatures(context)
def update_merge_armature(self: PropertyGroup, context: Context) -> None:
"""Update function for merge_armature property"""
auto_populate_merge_armatures(context)
@bpy.app.handlers.persistent
def depsgraph_update_handler(scene: Scene, depsgraph) -> None:
"""Handler to auto-populate merge armatures when objects change"""
# Check for any armature-related updates
armature_updated = False
for update in depsgraph.updates:
if hasattr(update, 'id') and update.id and hasattr(update.id, 'type'):
if update.id.type == 'ARMATURE':
armature_updated = True
break
if armature_updated:
# Use a timer to defer the update to avoid context issues
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
def auto_populate_safe() -> None:
"""Safe auto-populate function that can be called from timer"""
try:
if bpy.context and hasattr(bpy.context, 'scene') and hasattr(bpy.context.scene, 'avatar_toolkit'):
auto_populate_merge_armatures(bpy.context)
except (AttributeError, ReferenceError):
pass
return None # Don't repeat the timer
@bpy.app.handlers.persistent
def undo_post_handler(scene: Scene) -> None:
"""Handler for undo operations that might add/remove armatures"""
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
@bpy.app.handlers.persistent
def redo_post_handler(scene: Scene) -> None:
"""Handler for redo operations that might add/remove armatures"""
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=0.1)
class AvatarToolkitSceneProperties(PropertyGroup): class AvatarToolkitSceneProperties(PropertyGroup):
"""Property group containing Avatar Toolkit scene-level settings and properties""" """Property group containing Avatar Toolkit scene-level settings and properties"""
@@ -465,13 +533,15 @@ class AvatarToolkitSceneProperties(PropertyGroup):
merge_armature_into: StringProperty( merge_armature_into: StringProperty(
name=t('MergeArmature.into'), name=t('MergeArmature.into'),
description=t('MergeArmature.into_desc'), description=t('MergeArmature.into_desc'),
default="" default="",
update=update_merge_armature_into
) )
merge_armature: StringProperty( merge_armature: StringProperty(
name=t('MergeArmature.from'), name=t('MergeArmature.from'),
description=t('MergeArmature.from_desc'), description=t('MergeArmature.from_desc'),
default="" default="",
update=update_merge_armature
) )
attach_mesh: StringProperty( attach_mesh: StringProperty(
@@ -614,6 +684,15 @@ def register() -> None:
# Only register the property, not the classes (auto_load will handle that) # Only register the property, not the classes (auto_load will handle that)
bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties) bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties)
# Register handlers for auto-populating merge armatures
bpy.app.handlers.depsgraph_update_post.append(depsgraph_update_handler)
bpy.app.handlers.undo_post.append(undo_post_handler)
bpy.app.handlers.redo_post.append(redo_post_handler)
# Initial auto-populate
bpy.app.timers.register(lambda: auto_populate_safe(), first_interval=1.0)
logger.debug("Properties registered successfully") logger.debug("Properties registered successfully")
@@ -621,6 +700,14 @@ def unregister() -> None:
"""Unregister the Avatar Toolkit property group""" """Unregister the Avatar Toolkit property group"""
logger.info("Unregistering Avatar Toolkit properties") logger.info("Unregistering Avatar Toolkit properties")
# Remove handlers
if depsgraph_update_handler in bpy.app.handlers.depsgraph_update_post:
bpy.app.handlers.depsgraph_update_post.remove(depsgraph_update_handler)
if undo_post_handler in bpy.app.handlers.undo_post:
bpy.app.handlers.undo_post.remove(undo_post_handler)
if redo_post_handler in bpy.app.handlers.redo_post:
bpy.app.handlers.redo_post.remove(redo_post_handler)
# Remove the property # Remove the property
if hasattr(bpy.types.Scene, "avatar_toolkit"): if hasattr(bpy.types.Scene, "avatar_toolkit"):
try: try:
+47 -1
View File
@@ -8,6 +8,7 @@ from ...core.translations import t
import traceback import traceback
from ...core.common import ( from ...core.common import (
get_all_meshes, get_all_meshes,
get_meshes_for_armature,
fix_zero_length_bones, fix_zero_length_bones,
remove_unused_vertex_groups, remove_unused_vertex_groups,
clear_unused_data_blocks, clear_unused_data_blocks,
@@ -28,10 +29,32 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
return len(get_all_meshes(context)) > 1 # Check if we have valid armature selections for merging
base_armature_name: str = context.scene.avatar_toolkit.merge_armature_into
merge_armature_name: str = context.scene.avatar_toolkit.merge_armature
if not base_armature_name or not merge_armature_name:
return False
base_armature: Optional[Object] = bpy.data.objects.get(base_armature_name)
merge_armature: Optional[Object] = bpy.data.objects.get(merge_armature_name)
return (base_armature is not None and
merge_armature is not None and
base_armature.type == 'ARMATURE' and
merge_armature.type == 'ARMATURE' and
base_armature != merge_armature)
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
try: try:
# Store original mode to restore later
original_mode: str = context.mode
logger.debug(f"Original mode: {original_mode}")
# Switch to object mode if not already
if context.mode != 'OBJECT':
bpy.ops.object.mode_set(mode='OBJECT')
wm = context.window_manager wm = context.window_manager
wm.progress_begin(0, 100) wm.progress_begin(0, 100)
@@ -85,6 +108,18 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
merge_armature_obj = bpy.data.objects[merge_armature_name_stored] merge_armature_obj = bpy.data.objects[merge_armature_name_stored]
restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge) restore_breaking_settings_armature(merge_armature_obj, data_breaking_merge)
# Restore original mode if it wasn't OBJECT
try:
if original_mode == 'EDIT_ARMATURE':
bpy.ops.object.mode_set(mode='EDIT')
elif original_mode == 'POSE':
bpy.ops.object.mode_set(mode='POSE')
elif original_mode != 'OBJECT':
logger.debug(f"Restoring to original mode: {original_mode}")
# For other modes, stay in object mode as it's safest
except Exception:
logger.warning(f"Could not restore original mode: {original_mode}")
self.report({'INFO'}, t('MergeArmature.success')) self.report({'INFO'}, t('MergeArmature.success'))
return {'FINISHED'} return {'FINISHED'}
@@ -92,6 +127,17 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
except Exception as e: except Exception as e:
logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}") logger.error(f"Error merging armatures: {str(e)}\n{traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc()) self.report({'ERROR'}, traceback.format_exc())
# Try to restore original mode even on error
try:
if 'original_mode' in locals() and original_mode != 'OBJECT':
if original_mode == 'EDIT_ARMATURE':
bpy.ops.object.mode_set(mode='EDIT')
elif original_mode == 'POSE':
bpy.ops.object.mode_set(mode='POSE')
except Exception:
logger.warning("Could not restore mode after error")
return {'CANCELLED'} return {'CANCELLED'}
def delete_rigidbodies_and_joints(armature: Object) -> None: def delete_rigidbodies_and_joints(armature: Object) -> None:
+4 -2
View File
@@ -119,8 +119,10 @@ class AvatarToolkit_OT_ExplodeMesh(Operator):
@classmethod @classmethod
def poll(cls, context: Context) -> bool: def poll(cls, context: Context) -> bool:
active_obj = context.view_layer.objects.active
return context.view_layer.objects.active.type == "MESH" and len(context.view_layer.objects.selected) == 1 return (active_obj is not None and
active_obj.type == "MESH" and
len(context.view_layer.objects.selected) == 1)
+1 -7
View File
@@ -55,12 +55,6 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
logger.info(f"Starting armature standardization for {armature.name}") logger.info(f"Starting armature standardization for {armature.name}")
is_valid, _, _ = validate_armature(armature)
if is_valid:
logger.info("Armature already meets standards, no changes needed")
self.report({'INFO'}, t("Tools.standardize_already_valid"))
return {'FINISHED'}
original_mode: str = context.mode original_mode: str = context.mode
logger.debug(f"Original mode: {original_mode}") logger.debug(f"Original mode: {original_mode}")
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
@@ -90,7 +84,7 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
logger.info(f"Fixed {fixed_scale} scale issues") logger.info(f"Fixed {fixed_scale} scale issues")
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
is_valid, messages, _ = validate_armature(armature) is_valid, messages, _ = validate_armature(armature, override_mode='STRICT')
if is_valid: if is_valid:
logger.info("Armature successfully standardized") logger.info("Armature successfully standardized")
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.1)", "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.3.2)",
"AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there", "AvatarToolkit.desc1": "Avatar Toolkit is in Early Access there",
"AvatarToolkit.desc2": "will be issues, if you find any issues,", "AvatarToolkit.desc2": "will be issues, if you find any issues,",
"AvatarToolkit.desc3": "please report it on our Github.", "AvatarToolkit.desc3": "please report it on our Github.",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.1)", "AvatarToolkit.label": "アバターツールキット (アルファ 0.3.2)",
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
"AvatarToolkit.desc3": "GitHubで報告してください。", "AvatarToolkit.desc3": "GitHubで報告してください。",
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"authors": ["Avatar Toolkit Team"], "authors": ["Avatar Toolkit Team"],
"messages": { "messages": {
"AvatarToolkit.label": "아바타 툴킷 (알파 0.3.1)", "AvatarToolkit.label": "아바타 툴킷 (알파 0.3.2)",
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
"AvatarToolkit.desc3": "Github에 보고해 주세요.", "AvatarToolkit.desc3": "Github에 보고해 주세요.",