diff --git a/core/armature_validation.py b/core/armature_validation.py index f32ef1b..cd74c5b 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -5,21 +5,29 @@ from ..core.translations import t from ..core.dictionaries import ( standard_bones, bone_hierarchy, - finger_hierarchy + finger_hierarchy, + acceptable_bone_hierarchy, + acceptable_bone_names ) -def validate_armature(armature: Object) -> Tuple[bool, List[str]]: +def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: + """ + Validates armature and returns (is_valid, messages, is_acceptable_standard) + """ validation_mode = bpy.context.scene.avatar_toolkit.validation_mode messages: List[str] = [] if validation_mode == 'NONE': - return True, [] + return True, [], False if not armature or armature.type != 'ARMATURE' or not armature.data.bones: - return False, [t("Armature.validation.basic_check_failed")] + return False, [t("Armature.validation.basic_check_failed")], False found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones} + # Check if armature matches acceptable standards + is_acceptable = check_acceptable_standards(found_bones) + # List all bones in armature bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()]) messages.append(t("Armature.validation.found_bones", bones=bone_list)) @@ -75,8 +83,17 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str]]: if not validate_finger_chain(found_bones, finger_chain): messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) - is_valid: bool = len(messages) == 0 - return is_valid, messages + is_valid = len(messages) == 0 + + if not is_valid and is_acceptable: + messages = [ + t("Armature.validation.acceptable_standard.success"), + t("Armature.validation.acceptable_standard.note"), + t("Armature.validation.acceptable_standard.option") + ] + return True, messages, True + + return is_valid, messages, False def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool: """Validate if there is a valid parent-child relationship between bones""" @@ -109,3 +126,23 @@ def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> boo if not validate_bone_hierarchy(bones, chain[i], chain[i + 1]): return False return True + +def check_acceptable_standards(bones: Dict[str, Bone]) -> bool: + """Check if armature matches acceptable non-standard hierarchy""" + # Check if bones exist in acceptable list + for bone_category, acceptable_names in acceptable_bone_names.items(): + found = False + for name in acceptable_names: + if name in bones: + found = True + break + if not found: + return False + + # Validate acceptable hierarchy + for parent, child in acceptable_bone_hierarchy: + if parent in bones and child in bones: + if not validate_bone_hierarchy(bones, parent, child): + return False + + return True diff --git a/core/dictionaries.py b/core/dictionaries.py index 573228a..ebcbb89 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -470,6 +470,62 @@ finger_hierarchy = { ] } +acceptable_bone_hierarchy = [ + # Right side chain + ('Hips', 'Chest'), + ('Chest', 'Shoulder.R'), + ('Shoulder.R', 'Arm.R'), + ('Arm.R', 'Elbow.R'), + ('Elbow.R', 'Wrist.R'), + ('Hips', 'Leg.R'), + ('Leg.R', 'Knee.R'), + ('Knee.R', 'Foot.R'), + ('Foot.R', 'Toes.R'), + + # Left side chain + ('Chest', 'Shoulder.L'), + ('Shoulder.L', 'Arm.L'), + ('Arm.L', 'Elbow.L'), + ('Elbow.L', 'Wrist.L'), + ('Hips', 'Leg.L'), + ('Leg.L', 'Knee.L'), + ('Knee.L', 'Foot.L'), + ('Foot.L', 'Toes.L'), + + # Head and Eyes + ('Chest', 'Neck'), + ('Neck', 'Head'), + ('Head', 'Eye_L'), + ('Head', 'Eye_R'), + ('Head', 'LeftEye'), + ('Head', 'RightEye') +] + +acceptable_bone_names = { + 'hips': ['Hips'], + 'chest': ['Chest'], + 'neck': ['Neck'], + 'head': ['Head'], + 'eye_l': ['Eye_L', 'LeftEye'], + 'eye_r': ['Eye_R', 'RightEye'], + 'shoulder_r': ['Shoulder.R'], + 'arm_r': ['Arm.R'], + 'elbow_r': ['Elbow.R'], + 'wrist_r': ['Wrist.R'], + 'leg_r': ['Leg.R'], + 'knee_r': ['Knee.R'], + 'foot_r': ['Foot.R'], + 'toes_r': ['Toes.R'], + 'shoulder_l': ['Shoulder.L'], + 'arm_l': ['Arm.L'], + 'elbow_l': ['Elbow.L'], + 'wrist_l': ['Wrist.L'], + 'leg_l': ['Leg.L'], + 'knee_l': ['Knee.L'], + 'foot_l': ['Foot.L'], + 'toes_l': ['Toes.L'] +} + rigify_unity_names = { "DEF-spine": "Hips", "DEF-spine.001": "Spine", diff --git a/core/properties.py b/core/properties.py index b944662..6b5f28d 100644 --- a/core/properties.py +++ b/core/properties.py @@ -396,12 +396,6 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=0 ) - merge_twist_bones: BoolProperty( - name=t("Tools.merge_twist_bones"), - description=t("Tools.merge_twist_bones_desc"), - default=True - ) - list_only_mode: BoolProperty( name=t("Tools.list_only_mode"), description=t("Tools.list_only_mode_desc"), @@ -521,19 +515,13 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=False ) show_non_standard: BoolProperty( - name="Show Non-Standard Bones", + name="Show Non-Standard Bones", default=False ) show_hierarchy: BoolProperty( name="Show Hierarchy Issues", default=False ) - - merge_twist_bones: BoolProperty( - name=t("Tools.merge_twist_bones"), - description=t("Tools.merge_twist_bones_desc"), - default=True - ) def register() -> None: """Register the Avatar Toolkit property group""" diff --git a/functions/custom_tools/mesh_attachment.py b/functions/custom_tools/mesh_attachment.py index 1c20a97..d660e76 100644 --- a/functions/custom_tools/mesh_attachment.py +++ b/functions/custom_tools/mesh_attachment.py @@ -27,8 +27,8 @@ class AvatarToolkit_OT_AttachMesh(Operator): armature: Optional[Object] = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> Set[str]: try: diff --git a/functions/pose_mode.py b/functions/pose_mode.py index 0c57f2e..6cf2b00 100644 --- a/functions/pose_mode.py +++ b/functions/pose_mode.py @@ -23,7 +23,7 @@ class BatchPoseOperationMixin: armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid and context.mode == 'POSE' def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]: @@ -46,7 +46,7 @@ class AvatarToolkit_OT_StartPoseMode(Operator): armature = get_active_armature(context) if not armature or context.mode == "POSE": return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid def execute(self, context: Context) -> Set[str]: diff --git a/functions/tools/additional_tools.py b/functions/tools/additional_tools.py index 7cee04c..91afaee 100644 --- a/functions/tools/additional_tools.py +++ b/functions/tools/additional_tools.py @@ -19,8 +19,8 @@ class AvatarToolkit_OT_ApplyTransforms(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid and context.mode == 'OBJECT' + valid, _, _ = validate_armature(armature) + return valid and context.mode == 'OBJECT' def execute(self, context: Context) -> Set[str]: try: @@ -67,8 +67,8 @@ class AvatarToolkit_OT_CleanShapekeys(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0 + valid, _, _ = validate_armature(armature) + return valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0 def execute(self, context: Context) -> Set[str]: try: diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index 26e0873..78e6c72 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -34,8 +34,8 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return (is_valid and + valid, _, _ = validate_armature(armature) + return (valid and context.mode == 'EDIT_ARMATURE' and context.selected_editable_bones is not None and len(context.selected_editable_bones) == 2) @@ -128,8 +128,8 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> set[str]: """Execute the constraint removal operation""" diff --git a/functions/tools/convert_resonite.py b/functions/tools/convert_resonite.py index be5d72d..a41678a 100644 --- a/functions/tools/convert_resonite.py +++ b/functions/tools/convert_resonite.py @@ -20,8 +20,8 @@ class AvatarToolkit_OT_ConvertResonite(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> Set[str]: armature = get_active_armature(context) diff --git a/functions/tools/merge_tools.py b/functions/tools/merge_tools.py index d5c5426..4078f91 100644 --- a/functions/tools/merge_tools.py +++ b/functions/tools/merge_tools.py @@ -19,8 +19,8 @@ class AvatarToolkit_OT_ConnectBones(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> Set[str]: try: diff --git a/functions/tools/mesh_separation.py b/functions/tools/mesh_separation.py index 78c58d9..96d8881 100644 --- a/functions/tools/mesh_separation.py +++ b/functions/tools/mesh_separation.py @@ -17,10 +17,10 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return (context.active_object and context.active_object.type == 'MESH' and - is_valid) + valid) def execute(self, context: Context) -> set[str]: """Execute the separation operation""" @@ -49,10 +49,10 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return (context.active_object and context.active_object.type == 'MESH' and - is_valid) + valid) def execute(self, context: Context) -> set[str]: """Execute the separation operation""" diff --git a/functions/tools/rigify_converter.py b/functions/tools/rigify_converter.py index b04bd89..8737454 100644 --- a/functions/tools/rigify_converter.py +++ b/functions/tools/rigify_converter.py @@ -1,10 +1,11 @@ import bpy from typing import Dict, List, Set, Optional, Tuple, Any from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint -from ...core.common import get_active_armature, validate_armature +from ...core.common import get_active_armature from ...core.logging_setup import logger from ...core.translations import t from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones +from ...core.armature_validation import validate_armature class AvatarToolkit_OT_ConvertRigifyToUnity(Operator): """Convert Rigify armature to Unity-compatible format""" diff --git a/functions/visemes.py b/functions/visemes.py index d396150..3492f0e 100644 --- a/functions/visemes.py +++ b/functions/visemes.py @@ -138,9 +138,8 @@ class ATOOLKIT_OT_preview_visemes(Operator): armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid and mesh_obj and mesh_obj.type == 'MESH' - def execute(self, context: Context) -> Set[str]: props = context.scene.avatar_toolkit @@ -197,7 +196,7 @@ class ATOOLKIT_OT_create_visemes(Operator): armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid and mesh_obj and mesh_obj.type == 'MESH' diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index 572081e..d20db3a 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -84,19 +84,31 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): # Armature Validation active_armature: Optional[Object] = get_active_armature(context) if active_armature: - is_valid, messages = validate_armature(active_armature) + is_valid, messages, is_acceptable = validate_armature(active_armature) info_box = col.box() if is_valid: - row = info_box.row() - split = row.split(factor=0.6) - split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') - stats = get_armature_stats(active_armature) - split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) - - if stats['has_pose']: - info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') + if is_acceptable: + # Show acceptable standard message + info_box.label(text=messages[0], icon='INFO') + info_box.label(text=messages[1]) + info_box.label(text=messages[2]) + + # Add standardize button + standardize_box = info_box.box() + standardize_box.operator("avatar_toolkit.standardize_armature", + text=t("QuickAccess.standardize_armature"), + icon='MODIFIER') + else: + row = info_box.row() + split = row.split(factor=0.6) + split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') + stats = get_armature_stats(active_armature) + split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) + + if stats['has_pose']: + info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') else: # Found Bones section validation_box = info_box.box() @@ -146,7 +158,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): sub_row.alert = True sub_row.label(text=message) - # Validation Mode Warnings - always show in info box + # Validation Mode Warnings validation_mode = context.scene.avatar_toolkit.validation_mode if validation_mode == 'BASIC': warning_row = info_box.box() @@ -184,3 +196,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): button_row.scale_y = 1.5 button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT') button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT') +