diff --git a/blender_manifest.toml b/blender_manifest.toml index 7b8d826..d93f2d4 100644 --- a/blender_manifest.toml +++ b/blender_manifest.toml @@ -20,4 +20,3 @@ wheels = [ "./wheels/lz4-4.3.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", "./wheels/lz4-4.3.3-cp311-cp311-win_amd64.whl" ] - diff --git a/core/dictionaries.py b/core/dictionaries.py index 36d17b2..d17c4f9 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -640,3 +640,296 @@ rigify_unnecessary_bones = [ 'pelvis.' ] +# Non-standard bone mappings to standard bones +non_standard_mappings = { + 'hips': [ + 'mixamorig:Hips', 'mixamorig_Hips', + 'ORG-spine', 'spine', 'root', + 'hip', 'pelvis' + ], + 'spine': [ + 'mixamorig:Spine', 'mixamorig_Spine', + 'ORG-spine.001', 'spine.001', + 'abdomenLower', 'lowerback' + ], + 'chest': [ + 'mixamorig:Spine1', 'mixamorig_Spine1', + 'ORG-spine.002', 'spine.002', + 'abdomenUpper', 'upperback', 'spine1' + ], + 'upper_chest': [ + 'mixamorig:Spine2', 'mixamorig_Spine2', + 'ORG-spine.003', 'spine.003', + 'chestLower', 'chest', 'spine2' + ], + 'neck': [ + 'mixamorig:Neck', 'mixamorig_Neck', + 'ORG-spine.004', 'spine.004', 'neck', + 'neckLower' + ], + 'head': [ + 'mixamorig:Head', 'mixamorig_Head', + 'ORG-spine.005', 'spine.005', 'face', 'head' + ], + + 'left_shoulder': [ + 'mixamorig:LeftShoulder', 'mixamorig_LeftShoulder', + 'ORG-shoulder.L', 'shoulder.L', + 'lCollar', 'lShldr', 'lClavicle' + ], + 'left_arm': [ + 'mixamorig:LeftArm', 'mixamorig_LeftArm', + 'ORG-upper_arm.L', 'upper_arm.L', + 'lShldrBend', 'lShldrTwist', 'lArm' + ], + 'left_elbow': [ + 'mixamorig:LeftForeArm', 'mixamorig_LeftForeArm', + 'ORG-forearm.L', 'forearm.L', + 'lForearmBend', 'lElbow', 'lForeArm' + ], + 'left_wrist': [ + 'mixamorig:LeftHand', 'mixamorig_LeftHand', + 'ORG-hand.L', 'hand.L', + 'lHand', 'lWrist' + ], + + 'right_shoulder': [ + 'mixamorig:RightShoulder', 'mixamorig_RightShoulder', + 'ORG-shoulder.R', 'shoulder.R', + 'rCollar', 'rShldr', 'rClavicle' + ], + 'right_arm': [ + 'mixamorig:RightArm', 'mixamorig_RightArm', + 'ORG-upper_arm.R', 'upper_arm.R', + 'rShldrBend', 'rShldrTwist', 'rArm' + ], + 'right_elbow': [ + 'mixamorig:RightForeArm', 'mixamorig_RightForeArm', + 'ORG-forearm.R', 'forearm.R', + 'rForearmBend', 'rElbow', 'rForeArm' + ], + 'right_wrist': [ + 'mixamorig:RightHand', 'mixamorig_RightHand', + 'ORG-hand.R', 'hand.R', + 'rHand', 'rWrist' + ], + + 'left_leg': [ + 'mixamorig:LeftUpLeg', 'mixamorig_LeftUpLeg', + 'ORG-thigh.L', 'thigh.L', + 'lThighBend', 'lThigh' + ], + 'left_knee': [ + 'mixamorig:LeftLeg', 'mixamorig_LeftLeg', + 'ORG-shin.L', 'shin.L', + 'lShin', 'lKnee', 'lLeg' + ], + 'left_ankle': [ + 'mixamorig:LeftFoot', 'mixamorig_LeftFoot', + 'ORG-foot.L', 'foot.L', + 'lFoot', 'lAnkle' + ], + 'left_toe': [ + 'mixamorig:LeftToeBase', 'mixamorig_LeftToeBase', + 'ORG-toe.L', 'toe.L', + 'lToe' + ], + + 'right_leg': [ + 'mixamorig:RightUpLeg', 'mixamorig_RightUpLeg', + 'ORG-thigh.R', 'thigh.R', + 'rThighBend', 'rThigh' + ], + 'right_knee': [ + 'mixamorig:RightLeg', 'mixamorig_RightLeg', + 'ORG-shin.R', 'shin.R', + 'rShin', 'rKnee', 'rLeg' + ], + 'right_ankle': [ + 'mixamorig:RightFoot', 'mixamorig_RightFoot', + 'ORG-foot.R', 'foot.R', + 'rFoot', 'rAnkle' + ], + 'right_toe': [ + 'mixamorig:RightToeBase', 'mixamorig_RightToeBase', + 'ORG-toe.R', 'toe.R', + 'rToe' + ], + + 'thumb_1_l': [ + 'mixamorig:LeftHandThumb1', 'mixamorig_LeftHandThumb1', + 'ORG-thumb.01.L', 'thumb.01.L', + 'lThumb1' + ], + 'thumb_2_l': [ + 'mixamorig:LeftHandThumb2', 'mixamorig_LeftHandThumb2', + 'ORG-thumb.02.L', 'thumb.02.L', + 'lThumb2' + ], + 'thumb_3_l': [ + 'mixamorig:LeftHandThumb3', 'mixamorig_LeftHandThumb3', + 'ORG-thumb.03.L', 'thumb.03.L', + 'lThumb3' + ], + + 'index_1_l': [ + 'mixamorig:LeftHandIndex1', 'mixamorig_LeftHandIndex1', + 'ORG-f_index.01.L', 'f_index.01.L', + 'lIndex1' + ], + 'index_2_l': [ + 'mixamorig:LeftHandIndex2', 'mixamorig_LeftHandIndex2', + 'ORG-f_index.02.L', 'f_index.02.L', + 'lIndex2' + ], + 'index_3_l': [ + 'mixamorig:LeftHandIndex3', 'mixamorig_LeftHandIndex3', + 'ORG-f_index.03.L', 'f_index.03.L', + 'lIndex3' + ], + + 'middle_1_l': [ + 'mixamorig:LeftHandMiddle1', 'mixamorig_LeftHandMiddle1', + 'ORG-f_middle.01.L', 'f_middle.01.L', + 'lMid1' + ], + 'middle_2_l': [ + 'mixamorig:LeftHandMiddle2', 'mixamorig_LeftHandMiddle2', + 'ORG-f_middle.02.L', 'f_middle.02.L', + 'lMid2' + ], + 'middle_3_l': [ + 'mixamorig:LeftHandMiddle3', 'mixamorig_LeftHandMiddle3', + 'ORG-f_middle.03.L', 'f_middle.03.L', + 'lMid3' + ], + + 'ring_1_l': [ + 'mixamorig:LeftHandRing1', 'mixamorig_LeftHandRing1', + 'ORG-f_ring.01.L', 'f_ring.01.L', + 'lRing1' + ], + 'ring_2_l': [ + 'mixamorig:LeftHandRing2', 'mixamorig_LeftHandRing2', + 'ORG-f_ring.02.L', 'f_ring.02.L', + 'lRing2' + ], + 'ring_3_l': [ + 'mixamorig:LeftHandRing3', 'mixamorig_LeftHandRing3', + 'ORG-f_ring.03.L', 'f_ring.03.L', + 'lRing3' + ], + + 'pinkie_1_l': [ + 'mixamorig:LeftHandPinky1', 'mixamorig_LeftHandPinky1', + 'ORG-f_pinky.01.L', 'f_pinky.01.L', + 'lPinky1' + ], + 'pinkie_2_l': [ + 'mixamorig:LeftHandPinky2', 'mixamorig_LeftHandPinky2', + 'ORG-f_pinky.02.L', 'f_pinky.02.L', + 'lPinky2' + ], + 'pinkie_3_l': [ + 'mixamorig:LeftHandPinky3', 'mixamorig_LeftHandPinky3', + 'ORG-f_pinky.03.L', 'f_pinky.03.L', + 'lPinky3' + ], + + 'thumb_1_r': [ + 'mixamorig:RightHandThumb1', 'mixamorig_RightHandThumb1', + 'ORG-thumb.01.R', 'thumb.01.R', + 'rThumb1' + ], + 'thumb_2_r': [ + 'mixamorig:RightHandThumb2', 'mixamorig_RightHandThumb2', + 'ORG-thumb.02.R', 'thumb.02.R', + 'rThumb2' + ], + 'thumb_3_r': [ + 'mixamorig:RightHandThumb3', 'mixamorig_RightHandThumb3', + 'ORG-thumb.03.R', 'thumb.03.R', + 'rThumb3' + ], + + 'index_1_r': [ + 'mixamorig:RightHandIndex1', 'mixamorig_RightHandIndex1', + 'ORG-f_index.01.R', 'f_index.01.R', + 'rIndex1' + ], + 'index_2_r': [ + 'mixamorig:RightHandIndex2', 'mixamorig_RightHandIndex2', + 'ORG-f_index.02.R', 'f_index.02.R', + 'rIndex2' + ], + 'index_3_r': [ + 'mixamorig:RightHandIndex3', 'mixamorig_RightHandIndex3', + 'ORG-f_index.03.R', 'f_index.03.R', + 'rIndex3' + ], + + 'middle_1_r': [ + 'mixamorig:RightHandMiddle1', 'mixamorig_RightHandMiddle1', + 'ORG-f_middle.01.R', 'f_middle.01.R', + 'rMid1' + ], + 'middle_2_r': [ + 'mixamorig:RightHandMiddle2', 'mixamorig_RightHandMiddle2', + 'ORG-f_middle.02.R', 'f_middle.02.R', + 'rMid2' + ], + 'middle_3_r': [ + 'mixamorig:RightHandMiddle3', 'mixamorig_RightHandMiddle3', + 'ORG-f_middle.03.R', 'f_middle.03.R', + 'rMid3' + ], + + 'ring_1_r': [ + 'mixamorig:RightHandRing1', 'mixamorig_RightHandRing1', + 'ORG-f_ring.01.R', 'f_ring.01.R', + 'rRing1' + ], + 'ring_2_r': [ + 'mixamorig:RightHandRing2', 'mixamorig_RightHandRing2', + 'ORG-f_ring.02.R', 'f_ring.02.R', + 'rRing2' + ], + 'ring_3_r': [ + 'mixamorig:RightHandRing3', 'mixamorig_RightHandRing3', + 'ORG-f_ring.03.R', 'f_ring.03.R', + 'rRing3' + ], + + 'pinkie_1_r': [ + 'mixamorig:RightHandPinky1', 'mixamorig_RightHandPinky1', + 'ORG-f_pinky.01.R', 'f_pinky.01.R', + 'rPinky1' + ], + 'pinkie_2_r': [ + 'mixamorig:RightHandPinky2', 'mixamorig_RightHandPinky2', + 'ORG-f_pinky.02.R', 'f_pinky.02.R', + 'rPinky2' + ], + 'pinkie_3_r': [ + 'mixamorig:RightHandPinky3', 'mixamorig_RightHandPinky3', + 'ORG-f_pinky.03.R', 'f_pinky.03.R', + 'rPinky3' + ], + + 'left_eye': [ + 'mixamorig:LeftEye', 'mixamorig_LeftEye', + 'ORG-eye.L', 'eye.L', + 'lEye' + ], + 'right_eye': [ + 'mixamorig:RightEye', 'mixamorig_RightEye', + 'ORG-eye.R', 'eye.R', + 'rEye' + ] +} + +for category, mappings in non_standard_mappings.items(): + if category in bone_names: + bone_names[category].extend(mappings) + else: + bone_names[category] = mappings \ No newline at end of file diff --git a/core/properties.py b/core/properties.py index 8b02f2d..e7886d0 100644 --- a/core/properties.py +++ b/core/properties.py @@ -570,6 +570,24 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=False ) + standardize_fix_names: BoolProperty( + name=t("Tools.standardize_fix_names"), + description=t("Tools.standardize_fix_names_desc"), + default=True + ) + + standardize_fix_hierarchy: BoolProperty( + name=t("Tools.standardize_fix_hierarchy"), + description=t("Tools.standardize_fix_hierarchy_desc"), + default=True + ) + + standardize_fix_scale: BoolProperty( + name=t("Tools.standardize_fix_scale"), + description=t("Tools.standardize_fix_scale_desc"), + default=True + ) + def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") diff --git a/functions/tools/standardize_armature.py b/functions/tools/standardize_armature.py new file mode 100644 index 0000000..f7ad52a --- /dev/null +++ b/functions/tools/standardize_armature.py @@ -0,0 +1,308 @@ +import bpy +import math +from typing import Dict, List, Set, Tuple, Optional, Any, Union +from bpy.types import Operator, Context, Object, EditBone, Bone +from ...core.translations import t +from ...core.logging_setup import logger +from ...core.common import get_active_armature, ProgressTracker +from ...core.armature_validation import validate_armature +from ...core.dictionaries import ( + standard_bones, + bone_names, + bone_hierarchy, + acceptable_bone_names, + acceptable_bone_hierarchy, + non_standard_mappings +) + +class AvatarToolkit_OT_StandardizeArmature(Operator): + """Standardize armature bone names and hierarchy to match Avatar Toolkit requirements""" + bl_idname: str = "avatar_toolkit.standardize_armature" + bl_label: str = t("Tools.standardize_armature") + bl_description: str = t("Tools.standardize_armature_desc") + bl_options: Set[str] = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context: Context) -> bool: + armature: Optional[Object] = get_active_armature(context) + return armature is not None and context.mode in {'OBJECT', 'EDIT_ARMATURE'} + + def invoke(self, context: Context, event: Any) -> Set[str]: + logger.debug("Invoking standardize armature dialog") + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context: Context) -> None: + layout = self.layout + toolkit = context.scene.avatar_toolkit + + layout.prop(toolkit, "standardize_fix_names") + layout.prop(toolkit, "standardize_fix_hierarchy") + layout.prop(toolkit, "standardize_fix_scale") + layout.separator() + layout.label(text=t("Tools.standardize_warning"), icon='ERROR') + + def execute(self, context: Context) -> Set[str]: + armature: Optional[Object] = get_active_armature(context) + toolkit = context.scene.avatar_toolkit + + if not armature: + logger.warning("No active armature found for standardization") + self.report({'ERROR'}, t("Validation.no_armature")) + return {'CANCELLED'} + + 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') + context.view_layer.objects.active = armature + bpy.ops.object.mode_set(mode='EDIT') + + try: + with ProgressTracker(context, 3, "Standardizing Armature") as progress: + # Step 1: Fix bone names + if toolkit.standardize_fix_names: + progress.step("Fixing bone names") + renamed_bones: Dict[str, str] = self.standardize_bone_names(armature) + logger.info(f"Renamed {len(renamed_bones)} bones") + for old_name, new_name in renamed_bones.items(): + logger.debug(f"Renamed bone: {old_name} -> {new_name}") + + # Step 2: Fix hierarchy + if toolkit.standardize_fix_hierarchy: + progress.step("Fixing bone hierarchy") + fixed_hierarchy: int = self.standardize_bone_hierarchy(armature) + logger.info(f"Fixed {fixed_hierarchy} hierarchy relationships") + + # Step 3: Fix scale issues + if toolkit.standardize_fix_scale: + progress.step("Fixing bone scale") + fixed_scale: int = self.standardize_bone_scale(armature) + logger.info(f"Fixed {fixed_scale} scale issues") + + bpy.ops.object.mode_set(mode='OBJECT') + is_valid, messages, _ = validate_armature(armature) + + if is_valid: + logger.info("Armature successfully standardized") + self.report({'INFO'}, t("Tools.standardize_success")) + else: + logger.warning(f"Armature partially standardized. {len(messages)} issues remain") + bpy.ops.avatar_toolkit.standardize_issues_popup('INVOKE_DEFAULT') + self.report({'WARNING'}, t("Tools.standardize_partial")) + + if original_mode == 'EDIT_ARMATURE': + bpy.ops.object.mode_set(mode='EDIT') + + return {'FINISHED'} + + except Exception as e: + logger.error(f"Failed to standardize armature: {str(e)}") + self.report({'ERROR'}, str(e)) + + try: + if original_mode == 'EDIT_ARMATURE': + bpy.ops.object.mode_set(mode='EDIT') + else: + bpy.ops.object.mode_set(mode='OBJECT') + except Exception as restore_error: + logger.error(f"Failed to restore original mode: {str(restore_error)}") + + return {'CANCELLED'} + + def standardize_bone_names(self, armature: Object) -> Dict[str, str]: + """Rename bones to match standard naming conventions""" + logger.debug("Starting bone name standardization") + renamed_bones: Dict[str, str] = {} + edit_bones = armature.data.edit_bones + + # First, check which standard bones already exist + existing_standard_bones: Set[str] = set() + for bone in edit_bones: + if bone.name in standard_bones.values(): + existing_standard_bones.add(bone.name) + logger.debug(f"Found existing standard bone: {bone.name}") + + # Build a mapping of non-standard bone names to standard names + name_mapping: Dict[str, str] = {} + for category, standard_name in standard_bones.items(): + # Skip if this standard bone already exists + if standard_name in existing_standard_bones: + continue + + # Get all variants for this category + if category in non_standard_mappings: + for variant in non_standard_mappings[category]: + name_mapping[variant.lower()] = standard_name + + # First pass: identify bones to rename + bones_to_rename: Dict[str, str] = {} + for bone in edit_bones: + original_name: str = bone.name + + # Skip if this is already a standard bone name + if original_name in standard_bones.values(): + continue + + simplified_name: str = original_name.lower().replace(' ', '').replace('_', '').replace('.', '') + + # Check if this bone matches any known pattern + for variant, standard_name in name_mapping.items(): + # More precise matching - exact match or with common separators + if (variant == simplified_name or + variant == original_name.lower() or + f"{variant}_" in simplified_name or + f"{variant}." in simplified_name): + + if original_name != standard_name: + bones_to_rename[original_name] = standard_name + logger.debug(f"Identified bone to rename: {original_name} -> {standard_name}") + break + + # Special case for spine/chest hierarchy + # If we don't have an upper chest, don't rename chest to upper chest because it will break hierarchy + has_chest: bool = False + has_upper_chest: bool = False + + for bone_name in edit_bones.keys(): + if bone_name == standard_bones['chest']: + has_chest = True + elif bone_name == standard_bones['upper_chest']: + has_upper_chest = True + + # If we have a chest but no upper chest, don't rename anything to upper chest + if has_chest and not has_upper_chest: + for original_name, new_name in list(bones_to_rename.items()): + if new_name == standard_bones['upper_chest']: + logger.debug(f"Skipping upper chest rename for {original_name} as chest already exists") + del bones_to_rename[original_name] + + # Second pass: rename bones (in reverse to avoid naming conflicts) + for original_name, new_name in sorted(bones_to_rename.items(), reverse=True): + if original_name in edit_bones: + temp_name: str = f"TEMP_{original_name}" + edit_bones[original_name].name = temp_name + renamed_bones[original_name] = new_name + logger.debug(f"Temporarily renamed: {original_name} -> {temp_name}") + + # Third pass: apply final names + for original_name, new_name in renamed_bones.items(): + temp_name: str = f"TEMP_{original_name}" + if temp_name in edit_bones: + edit_bones[temp_name].name = new_name + logger.debug(f"Applied final rename: {temp_name} -> {new_name}") + + logger.info(f"Standardized {len(renamed_bones)} bone names") + return renamed_bones + + def standardize_bone_hierarchy(self, armature: Object) -> int: + """Fix bone hierarchy to match standard relationships""" + logger.debug("Starting bone hierarchy standardization") + edit_bones = armature.data.edit_bones + fixed_count: int = 0 + + # Build a mapping of standard bone names to their expected parents + hierarchy_map: Dict[str, str] = {} + for parent, child in bone_hierarchy: + if parent in edit_bones and child in edit_bones: + hierarchy_map[child] = parent + logger.debug(f"Found standard hierarchy: {parent} -> {child}") + + for parent, child in acceptable_bone_hierarchy: + if parent in edit_bones and child in edit_bones: + # Only add if not already in the map + if child not in hierarchy_map: + hierarchy_map[child] = parent + logger.debug(f"Found acceptable hierarchy: {parent} -> {child}") + + for child_name, parent_name in hierarchy_map.items(): + if child_name in edit_bones and parent_name in edit_bones: + child_bone: EditBone = edit_bones[child_name] + parent_bone: EditBone = edit_bones[parent_name] + + if child_bone.parent != parent_bone: + logger.debug(f"Fixing hierarchy: {child_name} parent was {child_bone.parent.name if child_bone.parent else 'None'}, setting to {parent_name}") + child_bone.parent = parent_bone + fixed_count += 1 + + logger.info(f"Fixed {fixed_count} bone hierarchy relationships") + return fixed_count + + def standardize_bone_scale(self, armature: Object) -> int: + """Fix bone scale issues by normalizing bone lengths""" + logger.debug("Starting bone scale standardization") + edit_bones = armature.data.edit_bones + fixed_count: int = 0 + + # Calculate median bone length for reference + lengths: List[float] = [bone.length for bone in edit_bones if bone.length > 0.0001] + if not lengths: + logger.warning("No valid bone lengths found for scale standardization") + return 0 + + lengths.sort() + median_length: float = lengths[len(lengths) // 2] + logger.debug(f"Median bone length: {median_length}") + + # Calculate mean and standard deviation + mean: float = sum(lengths) / len(lengths) + variance: float = sum((l - mean) ** 2 for l in lengths) / len(lengths) + std_dev: float = math.sqrt(variance) + logger.debug(f"Mean bone length: {mean}, Standard deviation: {std_dev}") + + small_threshold: float = max(median_length * 0.05, mean - 3 * std_dev) + large_threshold: float = min(median_length * 15, mean + 5 * std_dev) + logger.debug(f"Scale thresholds - small: {small_threshold}, large: {large_threshold}") + + for bone in edit_bones: + is_finger: bool = any(finger in bone.name.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']) + + if bone.length < small_threshold and not is_finger: + old_length: float = bone.length + bone.length = small_threshold + logger.debug(f"Fixed small bone {bone.name}: {old_length} -> {bone.length}") + fixed_count += 1 + elif bone.length > large_threshold: + old_length: float = bone.length + bone.length = large_threshold + logger.debug(f"Fixed large bone {bone.name}: {old_length} -> {bone.length}") + fixed_count += 1 + + logger.info(f"Fixed {fixed_count} bone scale issues") + return fixed_count + +class AvatarToolkit_OT_StandardizeIssuesPopup(Operator): + """Display information about remaining issues after standardization""" + bl_idname: str = "avatar_toolkit.standardize_issues_popup" + bl_label: str = t("Tools.standardize_issues_title") + bl_options: Set[str] = {'INTERNAL'} + + def execute(self, context: Context) -> Set[str]: + return {'FINISHED'} + + def invoke(self, context: Context, event: Any) -> Set[str]: + logger.debug("Showing standardization issues popup") + return context.window_manager.invoke_props_dialog(self, width=400) + + def draw(self, context: Context) -> None: + layout = self.layout + col = layout.column(align=True) + + col.label(text=t("Tools.standardize_issues_header"), icon='INFO') + col.separator() + + col.label(text=t("Tools.standardize_issues_line1")) + col.label(text=t("Tools.standardize_issues_line2")) + col.label(text=t("Tools.standardize_issues_line3")) + col.separator() + col.label(text=t("Tools.standardize_issues_line4")) + col.label(text=t("Tools.standardize_issues_line5")) + col.separator() + col.label(text=t("Tools.standardize_issues_line6")) + diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index bc67c62..388e8bd 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -239,6 +239,27 @@ "Tools.convert_rigify_to_unity_desc": "Convert Rigify armature to Unity-compatible format", "Tools.rigify_converted": "Rigify armature converted successfully", "Tools.no_armature": "No armature selected", + "Tools.standardize_title": "Standardization", + "Tools.standardize_armature": "Standardize Armature", + "Tools.standardize_armature_desc": "Convert non-standard armature to Avatar Toolkit standards", + "Tools.standardize_fix_names": "Fix Bone Names", + "Tools.standardize_fix_names_desc": "Rename bones to match standard naming conventions", + "Tools.standardize_fix_hierarchy": "Fix Bone Hierarchy", + "Tools.standardize_fix_hierarchy_desc": "Correct parent-child relationships between bones", + "Tools.standardize_fix_scale": "Fix Bone Scale", + "Tools.standardize_fix_scale_desc": "Normalize bone lengths to fix scale issues", + "Tools.standardize_warning": "This operation will modify your armature. Make a backup first!", + "Tools.standardize_success": "Armature successfully standardized", + "Tools.standardize_partial": "Armature partially standardized. Some issues remain.", + "Tools.standardize_already_valid": "Armature already meets standards. No changes needed.", + "Tools.standardize_issues_title": "Standardization Issues", + "Tools.standardize_issues_header": "Some issues still remain after standardization", + "Tools.standardize_issues_line1": "This could be because some bones on your avatar have unique names", + "Tools.standardize_issues_line2": "that aren't in our list of recognized non-standard bones.", + "Tools.standardize_issues_line3": "For example, if your hips bone is named 'THISISMYHIPS', we can't detect it.", + "Tools.standardize_issues_line4": "If your main skeleton bones aren't being recognized, please report this", + "Tools.standardize_issues_line5": "on our GitHub so we can add them to our database.", + "Tools.standardize_issues_line6": "Accessory bones (hair, clothing, etc.) must be renamed manually.", "UVTools.too_many_vertices": "Error! You have too much stuff selected. Are you sure you're selecting two edges?", "UVTools.need_line": "You need one line of selected UV points per selected object. Object \"{obj}\" does not meet this requirement!", diff --git a/ui/tools_panel.py b/ui/tools_panel.py index 7b4fda5..7fbfe71 100644 --- a/ui/tools_panel.py +++ b/ui/tools_panel.py @@ -55,6 +55,13 @@ class AvatarToolKit_PT_ToolsPanel(Panel): col.separator(factor=0.5) col.operator("avatar_toolkit.create_digitigrade", text=t("Tools.create_digitigrade"), icon='BONE_DATA') + # Standardization Tools + standardize_box: UILayout = bone_box.box() + col = standardize_box.column(align=True) + col.label(text=t("Tools.standardize_title"), icon='OUTLINER_OB_ARMATURE') + col.separator(factor=0.5) + col.operator("avatar_toolkit.standardize_armature", icon='CHECKMARK') + # Weight Tools weight_box: UILayout = bone_box.box() col = weight_box.column(align=True)