Merge pull request #187 from Yusarina/Current
Alpha 3: Avatar Toolkit 0.3.2
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
id = "avatar_toolkit"
|
||||
version = "0.3.1"
|
||||
version = "0.3.2"
|
||||
name = "Avatar Toolkit"
|
||||
tagline = "A modern tool for importing and optimizing models for VRChat, Resonite, and other similar games."
|
||||
maintainer = "Team NekoNeo"
|
||||
|
||||
@@ -15,12 +15,12 @@ from ..core.dictionaries import (
|
||||
)
|
||||
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
|
||||
"""
|
||||
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] = []
|
||||
hierarchy_messages: List[str] = []
|
||||
non_standard_messages: List[str] = []
|
||||
|
||||
@@ -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 []
|
||||
|
||||
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]:
|
||||
"""Validate mesh object for pose operations"""
|
||||
if not mesh_obj.data:
|
||||
|
||||
+89
-2
@@ -67,6 +67,74 @@ def get_mesh_objects(self, context):
|
||||
return [('NONE', t("Visemes.no_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):
|
||||
"""Property group containing Avatar Toolkit scene-level settings and properties"""
|
||||
|
||||
@@ -465,13 +533,15 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
||||
merge_armature_into: StringProperty(
|
||||
name=t('MergeArmature.into'),
|
||||
description=t('MergeArmature.into_desc'),
|
||||
default=""
|
||||
default="",
|
||||
update=update_merge_armature_into
|
||||
)
|
||||
|
||||
merge_armature: StringProperty(
|
||||
name=t('MergeArmature.from'),
|
||||
description=t('MergeArmature.from_desc'),
|
||||
default=""
|
||||
default="",
|
||||
update=update_merge_armature
|
||||
)
|
||||
|
||||
attach_mesh: StringProperty(
|
||||
@@ -614,6 +684,15 @@ def register() -> None:
|
||||
|
||||
# Only register the property, not the classes (auto_load will handle that)
|
||||
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")
|
||||
|
||||
|
||||
@@ -621,6 +700,14 @@ def unregister() -> None:
|
||||
"""Unregister the Avatar Toolkit property group"""
|
||||
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
|
||||
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
||||
try:
|
||||
|
||||
@@ -8,6 +8,7 @@ from ...core.translations import t
|
||||
import traceback
|
||||
from ...core.common import (
|
||||
get_all_meshes,
|
||||
get_meshes_for_armature,
|
||||
fix_zero_length_bones,
|
||||
remove_unused_vertex_groups,
|
||||
clear_unused_data_blocks,
|
||||
@@ -28,10 +29,32 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
||||
|
||||
@classmethod
|
||||
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]:
|
||||
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.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]
|
||||
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'))
|
||||
|
||||
return {'FINISHED'}
|
||||
@@ -92,6 +127,17 @@ class AvatarToolkit_OT_MergeArmature(bpy.types.Operator):
|
||||
except Exception as e:
|
||||
logger.error(f"Error merging armatures: {str(e)}\n{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'}
|
||||
|
||||
def delete_rigidbodies_and_joints(armature: Object) -> None:
|
||||
|
||||
@@ -119,8 +119,10 @@ class AvatarToolkit_OT_ExplodeMesh(Operator):
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
|
||||
return context.view_layer.objects.active.type == "MESH" and len(context.view_layer.objects.selected) == 1
|
||||
active_obj = context.view_layer.objects.active
|
||||
return (active_obj is not None and
|
||||
active_obj.type == "MESH" and
|
||||
len(context.view_layer.objects.selected) == 1)
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -55,12 +55,6 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||
|
||||
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
|
||||
logger.debug(f"Original mode: {original_mode}")
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
@@ -90,7 +84,7 @@ class AvatarToolkit_OT_StandardizeArmature(Operator):
|
||||
logger.info(f"Fixed {fixed_scale} scale issues")
|
||||
|
||||
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:
|
||||
logger.info("Armature successfully standardized")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"authors": ["Avatar Toolkit Team"],
|
||||
"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.desc2": "will be issues, if you find any issues,",
|
||||
"AvatarToolkit.desc3": "please report it on our Github.",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"authors": ["Avatar Toolkit Team"],
|
||||
"messages": {
|
||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.1)",
|
||||
"AvatarToolkit.label": "アバターツールキット (アルファ 0.3.2)",
|
||||
"AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、",
|
||||
"AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、",
|
||||
"AvatarToolkit.desc3": "GitHubで報告してください。",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"authors": ["Avatar Toolkit Team"],
|
||||
"messages": {
|
||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.3.1)",
|
||||
"AvatarToolkit.label": "아바타 툴킷 (알파 0.3.2)",
|
||||
"AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로",
|
||||
"AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면",
|
||||
"AvatarToolkit.desc3": "Github에 보고해 주세요.",
|
||||
|
||||
Reference in New Issue
Block a user