From 4b59147649ed1d78cbeff0690e8bdeca298ff147 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Fri, 7 Feb 2025 16:04:54 +0000 Subject: [PATCH] Moved Armature Validation to it's own file --- core/armature_validation.py | 88 +++++++++++++++++++++++ core/common.py | 83 --------------------- core/resonite_utils.py | 3 +- functions/custom_tools/mesh_attachment.py | 2 +- functions/eye_tracking.py | 2 +- functions/mmd_tools.py | 2 +- functions/optimization/materials_tools.py | 2 +- functions/optimization/mesh_tools.py | 2 +- functions/optimization/remove_doubles.py | 2 +- functions/pose_mode.py | 2 +- functions/tools/additional_tools.py | 3 +- functions/tools/bone_tools.py | 3 +- functions/tools/convert_resonite.py | 3 +- functions/tools/merge_tools.py | 3 +- functions/tools/mesh_separation.py | 3 +- functions/visemes.py | 2 +- ui/custom_avatar_panel.py | 2 +- ui/quick_access_panel.py | 2 +- 18 files changed, 109 insertions(+), 100 deletions(-) create mode 100644 core/armature_validation.py diff --git a/core/armature_validation.py b/core/armature_validation.py new file mode 100644 index 0000000..e7b4fa6 --- /dev/null +++ b/core/armature_validation.py @@ -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 diff --git a/core/common.py b/core/common.py index 45aecab..c00fb43 100644 --- a/core/common.py +++ b/core/common.py @@ -110,89 +110,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N if not armatures: return [('NONE', t("Armature.validation.no_armature"), '')] 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: """Automatically select armature if only one exists in scene""" diff --git a/core/resonite_utils.py b/core/resonite_utils.py index f6938d2..e9e530e 100644 --- a/core/resonite_utils.py +++ b/core/resonite_utils.py @@ -4,11 +4,12 @@ import bpy_extras from numpy import double 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 ..core.translations import t from ..core.dictionaries import bone_names, resonite_translations from ..core.logging_setup import logger +from ..core.armature_validation import validate_armature import re from .resonite_loader import resonite_animx, resonite_types diff --git a/functions/custom_tools/mesh_attachment.py b/functions/custom_tools/mesh_attachment.py index 613e5b9..1c20a97 100644 --- a/functions/custom_tools/mesh_attachment.py +++ b/functions/custom_tools/mesh_attachment.py @@ -7,12 +7,12 @@ from ...core.logging_setup import logger from ...core.translations import t from ...core.common import ( get_active_armature, - validate_armature, get_all_meshes, ProgressTracker, calculate_bone_orientation, add_armature_modifier ) +from ...core.armature_validation import validate_armature class AvatarToolkit_OT_AttachMesh(Operator): """Operator to attach a mesh to an armature bone with automatic weight setup""" diff --git a/functions/eye_tracking.py b/functions/eye_tracking.py index 6219ac7..8a6b69d 100644 --- a/functions/eye_tracking.py +++ b/functions/eye_tracking.py @@ -18,11 +18,11 @@ from ..core.common import ( get_active_armature, get_all_meshes, get_armature_list, - validate_armature, validate_mesh_for_pose, cache_vertex_positions, apply_vertex_positions ) +from ..core.armature_validation import validate_armature VALID_EYE_NAMES: Dict[str, List[str]] = { 'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'], diff --git a/functions/mmd_tools.py b/functions/mmd_tools.py index 884509a..c480aff 100644 --- a/functions/mmd_tools.py +++ b/functions/mmd_tools.py @@ -6,13 +6,13 @@ from ..core.logging_setup import logger from ..core.common import ( ProgressTracker, get_active_armature, - validate_armature, get_vertex_weights, transfer_vertex_weights, get_all_meshes ) from ..core.translations import t 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): """MMD Bone standardization system""" diff --git a/functions/optimization/materials_tools.py b/functions/optimization/materials_tools.py index d4f1607..95f54f8 100644 --- a/functions/optimization/materials_tools.py +++ b/functions/optimization/materials_tools.py @@ -14,10 +14,10 @@ from ...core.translations import t from ...core.common import ( get_active_armature, get_all_meshes, - validate_armature, clear_unused_data_blocks, ProgressTracker ) +from ...core.armature_validation import validate_armature def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool: """Compare two texture nodes for matching properties and image data""" diff --git a/functions/optimization/mesh_tools.py b/functions/optimization/mesh_tools.py index aac4a02..19a23c4 100644 --- a/functions/optimization/mesh_tools.py +++ b/functions/optimization/mesh_tools.py @@ -6,11 +6,11 @@ from ...core.translations import t from ...core.common import ( get_active_armature, get_all_meshes, - validate_armature, validate_meshes, join_mesh_objects, ProgressTracker ) +from ...core.armature_validation import validate_armature class AvatarToolkit_OT_JoinAllMeshes(Operator): """Operator to join all meshes in the scene""" diff --git a/functions/optimization/remove_doubles.py b/functions/optimization/remove_doubles.py index 8714e5b..3de462e 100644 --- a/functions/optimization/remove_doubles.py +++ b/functions/optimization/remove_doubles.py @@ -7,8 +7,8 @@ from ...core.translations import t from ...core.common import ( get_active_armature, get_all_meshes, - validate_armature ) +from ...core.armature_validation import validate_armature # Constants MERGE_ITERATION_COUNT = 20 diff --git a/functions/pose_mode.py b/functions/pose_mode.py index c8fbc15..0c57f2e 100644 --- a/functions/pose_mode.py +++ b/functions/pose_mode.py @@ -8,13 +8,13 @@ from ..core.common import ( get_active_armature, get_all_meshes, apply_pose_as_rest, - validate_armature, cache_vertex_positions, apply_vertex_positions, validate_mesh_for_pose, process_armature_modifiers, ProgressTracker ) +from ..core.armature_validation import validate_armature class BatchPoseOperationMixin: """Base class for batch pose operations""" diff --git a/functions/tools/additional_tools.py b/functions/tools/additional_tools.py index 2b3dd1c..7cee04c 100644 --- a/functions/tools/additional_tools.py +++ b/functions/tools/additional_tools.py @@ -4,7 +4,8 @@ from bpy.types import Operator, Context from typing import Set from ...core.translations import t 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): """Apply all transformations to armature and associated meshes""" diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index e90ceaf..a7b3247 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -5,12 +5,11 @@ from typing import Optional, Dict, Any, List, Tuple from ...core.translations import t from ...core.common import ( get_active_armature, - validate_armature, get_all_meshes, ProgressTracker, - validate_bone_hierarchy, restore_bone_transforms ) +from ...core.armature_validation import validate_armature, validate_bone_hierarchy def duplicate_bone(bone: EditBone) -> EditBone: """Create a duplicate of the given bone""" diff --git a/functions/tools/convert_resonite.py b/functions/tools/convert_resonite.py index 8ab5d99..be5d72d 100644 --- a/functions/tools/convert_resonite.py +++ b/functions/tools/convert_resonite.py @@ -4,8 +4,9 @@ from typing import Set, Dict, Optional from bpy.types import Operator, Context from ...core.translations import t 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.armature_validation import validate_armature class AvatarToolkit_OT_ConvertResonite(Operator): """Convert armature bone names to Resonite format with progress tracking and validation""" diff --git a/functions/tools/merge_tools.py b/functions/tools/merge_tools.py index b8daada..d5c5426 100644 --- a/functions/tools/merge_tools.py +++ b/functions/tools/merge_tools.py @@ -4,7 +4,8 @@ from typing import Set, List from bpy.types import Operator, Context, Armature, EditBone from ...core.translations import t 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): """Connect disconnected bones in chain""" diff --git a/functions/tools/mesh_separation.py b/functions/tools/mesh_separation.py index 6ffb68d..78c58d9 100644 --- a/functions/tools/mesh_separation.py +++ b/functions/tools/mesh_separation.py @@ -1,7 +1,8 @@ import bpy from bpy.types import Operator, Context 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): """Operator to separate mesh by materials""" diff --git a/functions/visemes.py b/functions/visemes.py index 5052559..d396150 100644 --- a/functions/visemes.py +++ b/functions/visemes.py @@ -9,10 +9,10 @@ from ..core.logging_setup import logger from ..core.translations import t from ..core.common import ( get_active_armature, - validate_armature, get_all_meshes, validate_mesh_for_pose ) +from ..core.armature_validation import validate_armature class VisemeCache: """Manages caching of generated viseme shape data for performance optimization""" diff --git a/ui/custom_avatar_panel.py b/ui/custom_avatar_panel.py index 1db821f..be7f6a9 100644 --- a/ui/custom_avatar_panel.py +++ b/ui/custom_avatar_panel.py @@ -6,9 +6,9 @@ from ..core.translations import t from ..core.common import ( get_active_armature, get_all_meshes, - validate_armature, get_armature_list ) +from ..core.armature_validation import validate_armature class AvatarToolkit_OT_SearchMergeArmatureInto(Operator): """Search operator for selecting target armature to merge into""" diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index 534d40d..ca712e9 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -14,7 +14,6 @@ from ..core.translations import t from ..core.common import ( get_active_armature, clear_default_objects, - validate_armature, get_armature_list, get_armature_stats ) @@ -24,6 +23,7 @@ from ..functions.pose_mode import ( AvatarToolkit_OT_ApplyPoseAsShapekey, AvatarToolkit_OT_ApplyPoseAsRest ) +from ..core.armature_validation import validate_armature class AvatarToolKit_OT_ExportFBX(Operator): """Export selected objects as FBX"""