Merge pull request #127 from Yusarina/Amrature-Validation-P2
Bone Standardization
This commit is contained in:
@@ -20,4 +20,3 @@ wheels = [
|
||||
"./wheels/lz4-4.4.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||
"./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl"
|
||||
]
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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!",
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user