diff --git a/blender_manifest.toml b/blender_manifest.toml index c66aece..1dcb437 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -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" diff --git a/core/armature_validation.py b/core/armature_validation.py index 3b5330b..570c098 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -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] = [] diff --git a/core/common.py b/core/common.py index e3a9067..e3edf1b 100644 --- a/core/common.py +++ b/core/common.py @@ -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: diff --git a/core/properties.py b/core/properties.py index ef9243d..23a9517 100644 --- a/core/properties.py +++ b/core/properties.py @@ -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: diff --git a/functions/custom_tools/armature_merging.py b/functions/custom_tools/armature_merging.py index 0a73586..1e2cb95 100644 --- a/functions/custom_tools/armature_merging.py +++ b/functions/custom_tools/armature_merging.py @@ -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: diff --git a/functions/tools/general_mesh_tools.py b/functions/tools/general_mesh_tools.py index 5695f15..b43512c 100644 --- a/functions/tools/general_mesh_tools.py +++ b/functions/tools/general_mesh_tools.py @@ -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) diff --git a/functions/tools/standardize_armature.py b/functions/tools/standardize_armature.py index 7d5fdcf..23039c3 100644 --- a/functions/tools/standardize_armature.py +++ b/functions/tools/standardize_armature.py @@ -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") diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index acc44fa..db2ae94 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "Avatar Toolkit (Alpha 0.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.", diff --git a/resources/translations/ja_JP.json b/resources/translations/ja_JP.json index 23d54f9..a43e8a1 100644 --- a/resources/translations/ja_JP.json +++ b/resources/translations/ja_JP.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "アバターツールキット (アルファ 0.3.1)", + "AvatarToolkit.label": "アバターツールキット (アルファ 0.3.2)", "AvatarToolkit.desc1": "アバターツールキットは早期アクセス中であり、", "AvatarToolkit.desc2": "問題が発生する可能性があります。問題を見つけた場合は、", "AvatarToolkit.desc3": "GitHubで報告してください。", diff --git a/resources/translations/ko_KR.json b/resources/translations/ko_KR.json index c16b5ae..2f9ab7f 100644 --- a/resources/translations/ko_KR.json +++ b/resources/translations/ko_KR.json @@ -1,7 +1,7 @@ { "authors": ["Avatar Toolkit Team"], "messages": { - "AvatarToolkit.label": "아바타 툴킷 (알파 0.3.1)", + "AvatarToolkit.label": "아바타 툴킷 (알파 0.3.2)", "AvatarToolkit.desc1": "아바타 툴킷은 초기 액세스 단계에 있으므로", "AvatarToolkit.desc2": "문제가 있을 수 있습니다. 문제를 발견하시면", "AvatarToolkit.desc3": "Github에 보고해 주세요.",