Moved Armature Validation to it's own file

This commit is contained in:
Yusarina
2025-02-07 16:04:54 +00:00
parent 3eb0029b5e
commit 4b59147649
18 changed files with 109 additions and 100 deletions
+88
View File
@@ -0,0 +1,88 @@
import bpy
from typing import Tuple, List, Dict, Set
from bpy.types import Object, Bone
from ..core.translations import t
from ..core.dictionaries import bone_names
def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
"""Enhanced armature validation with multiple validation modes"""
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = []
if validation_mode == 'NONE':
return True, []
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")]
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones: List[str] = []
for bone in essential_bones:
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
missing_bones.append(bone)
if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
if validation_mode == 'STRICT':
hierarchy: List[Tuple[str, str]] = [
('hips', 'spine'), ('spine', 'chest'),
('chest', 'neck'), ('neck', 'head')
]
for parent, child in hierarchy:
if not validate_bone_hierarchy(found_bones, parent, child):
messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
is_valid: bool = len(messages) == 0
return is_valid, messages
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"""
parent_bone: Optional[Bone] = None
child_bone: Optional[Bone] = None
for alt_name in bone_names[parent_name]:
if alt_name in bones:
parent_bone = bones[alt_name]
break
for alt_name in bone_names[child_name]:
if alt_name in bones:
child_bone = bones[alt_name]
break
if not parent_bone or not child_bone:
return False
return child_bone.parent == parent_bone
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
"""Validate if matching left and right bones exist for a given base bone name"""
left_patterns: List[str] = [
f"{base}.{left}",
f"{base}_{left}",
f"{left}_{base}"
]
right_patterns: List[str] = [
f"{base}.{right}",
f"{base}_{right}",
f"{right}_{base}"
]
left_exists: bool = any(pattern in bones for pattern in left_patterns)
right_exists: bool = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists
-83
View File
@@ -111,89 +111,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
return [('NONE', t("Armature.validation.no_armature"), '')] return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures return armatures
def validate_armature(armature: Object) -> Tuple[bool, List[str]]:
"""Enhanced armature validation with multiple validation modes"""
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = []
if validation_mode == 'NONE':
return True, []
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")]
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones}
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones: List[str] = []
for bone in essential_bones:
if not any(alt_name in found_bones for alt_name in bone_names[bone]):
missing_bones.append(bone)
if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
if validation_mode == 'STRICT':
hierarchy: List[Tuple[str, str]] = [
('hips', 'spine'), ('spine', 'chest'),
('chest', 'neck'), ('neck', 'head')
]
for parent, child in hierarchy:
if not validate_bone_hierarchy(found_bones, parent, child):
messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
symmetry_pairs: List[Tuple[str, str, str]] = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
is_valid: bool = len(messages) == 0
return is_valid, messages
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"""
parent_bone: Optional[Bone] = None
child_bone: Optional[Bone] = None
for alt_name in bone_names[parent_name]:
if alt_name in bones:
parent_bone = bones[alt_name]
break
for alt_name in bone_names[child_name]:
if alt_name in bones:
child_bone = bones[alt_name]
break
if not parent_bone or not child_bone:
return False
return child_bone.parent == parent_bone
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool:
"""Validate if matching left and right bones exist for a given base bone name"""
left_patterns: List[str] = [
f"{base}.{left}",
f"{base}_{left}",
f"{left}_{base}"
]
right_patterns: List[str] = [
f"{base}.{right}",
f"{base}_{right}",
f"{right}_{base}"
]
left_exists: bool = any(pattern in bones for pattern in left_patterns)
right_exists: bool = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists
def auto_select_single_armature(context: Context) -> None: def auto_select_single_armature(context: Context) -> None:
"""Automatically select armature if only one exists in scene""" """Automatically select armature if only one exists in scene"""
armatures: List[Tuple[str, str, str]] = get_armature_list(context) armatures: List[Tuple[str, str, str]] = get_armature_list(context)
+2 -1
View File
@@ -4,11 +4,12 @@ import bpy_extras
from numpy import double from numpy import double
from typing import Set, Dict from typing import Set, Dict
from .common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker from .common import get_active_armature, simplify_bonename, ProgressTracker
from bpy.types import Context, Operator from bpy.types import Context, Operator
from ..core.translations import t from ..core.translations import t
from ..core.dictionaries import bone_names, resonite_translations from ..core.dictionaries import bone_names, resonite_translations
from ..core.logging_setup import logger from ..core.logging_setup import logger
from ..core.armature_validation import validate_armature
import re import re
from .resonite_loader import resonite_animx, resonite_types from .resonite_loader import resonite_animx, resonite_types
+1 -1
View File
@@ -7,12 +7,12 @@ from ...core.logging_setup import logger
from ...core.translations import t from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
validate_armature,
get_all_meshes, get_all_meshes,
ProgressTracker, ProgressTracker,
calculate_bone_orientation, calculate_bone_orientation,
add_armature_modifier add_armature_modifier
) )
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_AttachMesh(Operator): class AvatarToolkit_OT_AttachMesh(Operator):
"""Operator to attach a mesh to an armature bone with automatic weight setup""" """Operator to attach a mesh to an armature bone with automatic weight setup"""
+1 -1
View File
@@ -18,11 +18,11 @@ from ..core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
get_armature_list, get_armature_list,
validate_armature,
validate_mesh_for_pose, validate_mesh_for_pose,
cache_vertex_positions, cache_vertex_positions,
apply_vertex_positions apply_vertex_positions
) )
from ..core.armature_validation import validate_armature
VALID_EYE_NAMES: Dict[str, List[str]] = { VALID_EYE_NAMES: Dict[str, List[str]] = {
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'], 'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
+1 -1
View File
@@ -6,13 +6,13 @@ from ..core.logging_setup import logger
from ..core.common import ( from ..core.common import (
ProgressTracker, ProgressTracker,
get_active_armature, get_active_armature,
validate_armature,
get_vertex_weights, get_vertex_weights,
transfer_vertex_weights, transfer_vertex_weights,
get_all_meshes get_all_meshes
) )
from ..core.translations import t from ..core.translations import t
from ..core.dictionaries import bone_names, dont_delete_these_main_bones from ..core.dictionaries import bone_names, dont_delete_these_main_bones
from ..core.armature_validation import validate_armature, validate_bone_hierarchy
class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator): class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
"""MMD Bone standardization system""" """MMD Bone standardization system"""
+1 -1
View File
@@ -14,10 +14,10 @@ from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature,
clear_unused_data_blocks, clear_unused_data_blocks,
ProgressTracker ProgressTracker
) )
from ...core.armature_validation import validate_armature
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool: def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
"""Compare two texture nodes for matching properties and image data""" """Compare two texture nodes for matching properties and image data"""
+1 -1
View File
@@ -6,11 +6,11 @@ from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature,
validate_meshes, validate_meshes,
join_mesh_objects, join_mesh_objects,
ProgressTracker ProgressTracker
) )
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_JoinAllMeshes(Operator): class AvatarToolkit_OT_JoinAllMeshes(Operator):
"""Operator to join all meshes in the scene""" """Operator to join all meshes in the scene"""
+1 -1
View File
@@ -7,8 +7,8 @@ from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature
) )
from ...core.armature_validation import validate_armature
# Constants # Constants
MERGE_ITERATION_COUNT = 20 MERGE_ITERATION_COUNT = 20
+1 -1
View File
@@ -8,13 +8,13 @@ from ..core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
apply_pose_as_rest, apply_pose_as_rest,
validate_armature,
cache_vertex_positions, cache_vertex_positions,
apply_vertex_positions, apply_vertex_positions,
validate_mesh_for_pose, validate_mesh_for_pose,
process_armature_modifiers, process_armature_modifiers,
ProgressTracker ProgressTracker
) )
from ..core.armature_validation import validate_armature
class BatchPoseOperationMixin: class BatchPoseOperationMixin:
"""Base class for batch pose operations""" """Base class for batch pose operations"""
+2 -1
View File
@@ -4,7 +4,8 @@ from bpy.types import Operator, Context
from typing import Set from typing import Set
from ...core.translations import t from ...core.translations import t
from ...core.logging_setup import logger from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, validate_armature, remove_unused_shapekeys from ...core.common import get_active_armature, get_all_meshes, remove_unused_shapekeys
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_ApplyTransforms(Operator): class AvatarToolkit_OT_ApplyTransforms(Operator):
"""Apply all transformations to armature and associated meshes""" """Apply all transformations to armature and associated meshes"""
+1 -2
View File
@@ -5,12 +5,11 @@ from typing import Optional, Dict, Any, List, Tuple
from ...core.translations import t from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
validate_armature,
get_all_meshes, get_all_meshes,
ProgressTracker, ProgressTracker,
validate_bone_hierarchy,
restore_bone_transforms restore_bone_transforms
) )
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
def duplicate_bone(bone: EditBone) -> EditBone: def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone""" """Create a duplicate of the given bone"""
+2 -1
View File
@@ -4,8 +4,9 @@ from typing import Set, Dict, Optional
from bpy.types import Operator, Context from bpy.types import Operator, Context
from ...core.translations import t from ...core.translations import t
from ...core.logging_setup import logger from ...core.logging_setup import logger
from ...core.common import get_active_armature, simplify_bonename, validate_armature, ProgressTracker from ...core.common import get_active_armature, simplify_bonename, ProgressTracker
from ...core.dictionaries import bone_names, resonite_translations from ...core.dictionaries import bone_names, resonite_translations
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_ConvertResonite(Operator): class AvatarToolkit_OT_ConvertResonite(Operator):
"""Convert armature bone names to Resonite format with progress tracking and validation""" """Convert armature bone names to Resonite format with progress tracking and validation"""
+2 -1
View File
@@ -4,7 +4,8 @@ from typing import Set, List
from bpy.types import Operator, Context, Armature, EditBone from bpy.types import Operator, Context, Armature, EditBone
from ...core.translations import t from ...core.translations import t
from ...core.logging_setup import logger from ...core.logging_setup import logger
from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights, validate_armature from ...core.common import get_active_armature, get_all_meshes, get_vertex_weights, transfer_vertex_weights
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_ConnectBones(Operator): class AvatarToolkit_OT_ConnectBones(Operator):
"""Connect disconnected bones in chain""" """Connect disconnected bones in chain"""
+2 -1
View File
@@ -1,7 +1,8 @@
import bpy import bpy
from bpy.types import Operator, Context from bpy.types import Operator, Context
from ...core.translations import t from ...core.translations import t
from ...core.common import get_active_armature, validate_armature from ...core.common import get_active_armature
from ...core.armature_validation import validate_armature
class AvatarToolKit_OT_SeparateByMaterials(Operator): class AvatarToolKit_OT_SeparateByMaterials(Operator):
"""Operator to separate mesh by materials""" """Operator to separate mesh by materials"""
+1 -1
View File
@@ -9,10 +9,10 @@ from ..core.logging_setup import logger
from ..core.translations import t from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
validate_armature,
get_all_meshes, get_all_meshes,
validate_mesh_for_pose validate_mesh_for_pose
) )
from ..core.armature_validation import validate_armature
class VisemeCache: class VisemeCache:
"""Manages caching of generated viseme shape data for performance optimization""" """Manages caching of generated viseme shape data for performance optimization"""
+1 -1
View File
@@ -6,9 +6,9 @@ from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature,
get_armature_list get_armature_list
) )
from ..core.armature_validation import validate_armature
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator): class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
"""Search operator for selecting target armature to merge into""" """Search operator for selecting target armature to merge into"""
+1 -1
View File
@@ -14,7 +14,6 @@ from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
clear_default_objects, clear_default_objects,
validate_armature,
get_armature_list, get_armature_list,
get_armature_stats get_armature_stats
) )
@@ -24,6 +23,7 @@ from ..functions.pose_mode import (
AvatarToolkit_OT_ApplyPoseAsShapekey, AvatarToolkit_OT_ApplyPoseAsShapekey,
AvatarToolkit_OT_ApplyPoseAsRest AvatarToolkit_OT_ApplyPoseAsRest
) )
from ..core.armature_validation import validate_armature
class AvatarToolKit_OT_ExportFBX(Operator): class AvatarToolKit_OT_ExportFBX(Operator):
"""Export selected objects as FBX""" """Export selected objects as FBX"""