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-manylinux_2_17_x86_64.manylinux2014_x86_64.whl",
|
||||||
"./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl"
|
"./wheels/lz4-4.4.3-cp311-cp311-win_amd64.whl"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -640,3 +640,296 @@ rigify_unnecessary_bones = [
|
|||||||
'pelvis.'
|
'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
|
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:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
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.convert_rigify_to_unity_desc": "Convert Rigify armature to Unity-compatible format",
|
||||||
"Tools.rigify_converted": "Rigify armature converted successfully",
|
"Tools.rigify_converted": "Rigify armature converted successfully",
|
||||||
"Tools.no_armature": "No armature selected",
|
"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.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!",
|
"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.separator(factor=0.5)
|
||||||
col.operator("avatar_toolkit.create_digitigrade", text=t("Tools.create_digitigrade"), icon='BONE_DATA')
|
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 Tools
|
||||||
weight_box: UILayout = bone_box.box()
|
weight_box: UILayout = bone_box.box()
|
||||||
col = weight_box.column(align=True)
|
col = weight_box.column(align=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user