From 148263240598e9cdf5b53c23e5140ef89475e20e Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 1 Feb 2025 21:39:43 +0000 Subject: [PATCH 1/5] Improvement to logging. --- core/logging_setup.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/core/logging_setup.py b/core/logging_setup.py index 2f65ce7..6769c2e 100644 --- a/core/logging_setup.py +++ b/core/logging_setup.py @@ -4,6 +4,7 @@ from typing import Optional, Any from bpy.types import Context logger = logging.getLogger('avatar_toolkit') +_original_error = logger.error def configure_logging(enabled: bool = False) -> None: """Configure logging for Avatar Toolkit""" @@ -16,15 +17,16 @@ def configure_logging(enabled: bool = False) -> None: if enabled: handler = logging.StreamHandler() handler.setLevel(logging.DEBUG) - formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s\n%(exc_info)s' if enabled else '%(message)s') + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) - # Override error logging to include traceback def error_with_traceback(msg, *args, **kwargs): - if kwargs.get('exc_info', False): - msg = f"{msg}\n{traceback.format_exc()}" - logger.error(msg, *args, **kwargs) + if kwargs.get('exc_info', False) or isinstance(msg, Exception): + full_msg = f"{msg}\n{traceback.format_exc()}" + _original_error(full_msg, *args, **{**kwargs, 'exc_info': False}) + else: + _original_error(msg, *args, **kwargs) logger.error = error_with_traceback From 4b59147649ed1d78cbeff0690e8bdeca298ff147 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Fri, 7 Feb 2025 16:04:54 +0000 Subject: [PATCH 2/5] 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""" From 6412b6f619f8ba9762091b7a768dca1f6200c4c3 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Fri, 7 Feb 2025 18:18:09 +0000 Subject: [PATCH 3/5] Better checks - Added standard list. - Added bone_hierarchy list - Added bone_hierarchy - Better checks. - Better UI. This is the first part, still needs alot of work, but this is better then before. Need to add some more standards and then we will be golden. --- core/armature_validation.py | 93 ++++++++++++++--------- core/dictionaries.py | 118 ++++++++++++++++++++++++++++++ core/properties.py | 13 ++++ resources/translations/en_US.json | 9 +++ ui/quick_access_panel.py | 62 +++++++++++++--- 5 files changed, 250 insertions(+), 45 deletions(-) diff --git a/core/armature_validation.py b/core/armature_validation.py index e7b4fa6..f32ef1b 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -1,11 +1,14 @@ import bpy -from typing import Tuple, List, Dict, Set +from typing import Tuple, List, Dict, Set, Optional from bpy.types import Object, Bone from ..core.translations import t -from ..core.dictionaries import bone_names +from ..core.dictionaries import ( + standard_bones, + bone_hierarchy, + finger_hierarchy +) 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] = [] @@ -15,28 +18,48 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str]]: 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'} + found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones} - 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) + # List all bones in armature + bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()]) + messages.append(t("Armature.validation.found_bones", bones=bone_list)) + + # Check each bone against our standard + non_standard_bones = [] + required_patterns = [ + 'Hips', 'Spine', 'Chest', 'Neck', 'Head', + 'Upper', 'Lower', 'Hand', 'Foot', 'Toe', + 'Thumb', 'Index', 'Middle', 'Ring', 'Pinky', + 'Eye' + ] + + for bone_name in found_bones: + if any(pattern in bone_name for pattern in required_patterns): + if bone_name not in standard_bones.values(): + non_standard_bones.append(bone_name) + + if non_standard_bones: + non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones]) + messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list)) + + # Check for missing required bones + essential_bones = {standard_bones[key] for key in ['hips', 'spine', 'chest', 'neck', 'head']} + missing_bones = [bone for bone in essential_bones if bone not in found_bones] if missing_bones: - messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones))) + missing_list = "\n".join([f"- {bone}" for bone in missing_bones]) + messages.append(t("Armature.validation.missing_bones", bones=missing_list)) 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)) + # Validate bone hierarchy + for parent, child in bone_hierarchy: + if parent in found_bones and child in found_bones: + 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')] + # Validate symmetry + symmetry_pairs = [('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)) @@ -44,29 +67,22 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str]]: 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")) + + # Validate finger hierarchies + for side in ['left', 'right']: + for finger_chain in finger_hierarchy[side]: + if all(bone in found_bones for bone in finger_chain): + if not validate_finger_chain(found_bones, finger_chain): + messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) 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: + if parent_name not in bones or child_name not in bones: return False - - return child_bone.parent == parent_bone + return bones[child_name].parent == bones[parent_name] 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""" @@ -86,3 +102,10 @@ def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) right_exists: bool = any(pattern in bones for pattern in right_patterns) return left_exists and right_exists + +def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> bool: + """Validate if a finger bone chain has correct hierarchy""" + for i in range(len(chain) - 1): + if not validate_bone_hierarchy(bones, chain[i], chain[i + 1]): + return False + return True diff --git a/core/dictionaries.py b/core/dictionaries.py index 9a54b05..88925bf 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -354,3 +354,121 @@ resonite_translations = { 'thumb_2_r': "thumb2.R", 'thumb_3_r': "thumb3.R" } + +standard_bones = { + # Core Structure + 'hips': 'Hips', + 'spine': 'Spine', + 'chest': 'Chest', + 'upper_chest': 'Chest.Up', + 'neck': 'Neck', + 'head': 'Head', + + # Arms + 'left_arm': 'UpperArm.L', + 'left_elbow': 'LowerArm.L', + 'left_wrist': 'Hand.L', + 'right_arm': 'UpperArm.R', + 'right_elbow': 'LowerArm.R', + 'right_wrist': 'Hand.R', + + # Legs + 'left_leg': 'UpperLeg.L', + 'left_knee': 'LowerLeg.L', + 'left_ankle': 'Foot.L', + 'left_toe': 'Toes.L', + 'right_leg': 'UpperLeg.R', + 'right_knee': 'LowerLeg.R', + 'right_ankle': 'Foot.R', + 'right_toe': 'Toes.R', + + # Fingers Left + 'thumb_1_l': 'Thumb1.L', + 'thumb_2_l': 'Thumb2.L', + 'thumb_3_l': 'Thumb3.L', + 'index_1_l': 'Index1.L', + 'index_2_l': 'Index2.L', + 'index_3_l': 'Index3.L', + 'middle_1_l': 'Middle1.L', + 'middle_2_l': 'Middle2.L', + 'middle_3_l': 'Middle3.L', + 'ring_1_l': 'Ring1.L', + 'ring_2_l': 'Ring2.L', + 'ring_3_l': 'Ring3.L', + 'pinkie_1_l': 'Pinky1.L', + 'pinkie_2_l': 'Pinky2.L', + 'pinkie_3_l': 'Pinky3.L', + + # Fingers Right + 'thumb_1_r': 'Thumb1.R', + 'thumb_2_r': 'Thumb2.R', + 'thumb_3_r': 'Thumb3.R', + 'index_1_r': 'Index1.R', + 'index_2_r': 'Index2.R', + 'index_3_r': 'Index3.R', + 'middle_1_r': 'Middle1.R', + 'middle_2_r': 'Middle2.R', + 'middle_3_r': 'Middle3.R', + 'ring_1_r': 'Ring1.R', + 'ring_2_r': 'Ring2.R', + 'ring_3_r': 'Ring3.R', + 'pinkie_1_r': 'Pinky1.R', + 'pinkie_2_r': 'Pinky2.R', + 'pinkie_3_r': 'Pinky3.R', + + # Eyes + 'left_eye': 'Eye.L', + 'right_eye': 'Eye.R' +} + +# Define standard bone hierarchies +bone_hierarchy = [ + ('Hips', 'Spine'), + ('Spine', 'Chest'), + ('Chest', 'Chest.Up'), + ('Chest.Up', 'Neck'), + ('Neck', 'Head'), + ('Head', 'Eye.L'), + ('Head', 'Eye.R'), + + # Left Arm Chain + ('Chest.Up', 'UpperArm.L'), + ('UpperArm.L', 'LowerArm.L'), + ('LowerArm.L', 'Hand.L'), + + # Right Arm Chain + ('Chest.Up', 'UpperArm.R'), + ('UpperArm.R', 'LowerArm.R'), + ('LowerArm.R', 'Hand.R'), + + # Left Leg Chain + ('Hips', 'UpperLeg.L'), + ('UpperLeg.L', 'LowerLeg.L'), + ('LowerLeg.L', 'Foot.L'), + ('Foot.L', 'Toes.L'), + + # Right Leg Chain + ('Hips', 'UpperLeg.R'), + ('UpperLeg.R', 'LowerLeg.R'), + ('LowerLeg.R', 'Foot.R'), + ('Foot.R', 'Toes.R') +] + +# Define finger hierarchies +finger_hierarchy = { + 'left': [ + ('Hand.L', 'Thumb1.L', 'Thumb2.L', 'Thumb3.L'), + ('Hand.L', 'Index1.L', 'Index2.L', 'Index3.L'), + ('Hand.L', 'Middle1.L', 'Middle2.L', 'Middle3.L'), + ('Hand.L', 'Ring1.L', 'Ring2.L', 'Ring3.L'), + ('Hand.L', 'Pinky1.L', 'Pinky2.L', 'Pinky3.L') + ], + 'right': [ + ('Hand.R', 'Thumb1.R', 'Thumb2.R', 'Thumb3.R'), + ('Hand.R', 'Index1.R', 'Index2.R', 'Index3.R'), + ('Hand.R', 'Middle1.R', 'Middle2.R', 'Middle3.R'), + ('Hand.R', 'Ring1.R', 'Ring2.R', 'Ring3.R'), + ('Hand.R', 'Pinky1.R', 'Pinky2.R', 'Pinky3.R') + ] +} + diff --git a/core/properties.py b/core/properties.py index 969f1b4..7f32c25 100644 --- a/core/properties.py +++ b/core/properties.py @@ -462,6 +462,19 @@ class AvatarToolkitSceneProperties(PropertyGroup): materials: CollectionProperty( type=SceneMatClass ) + + show_found_bones: BoolProperty( + name="Show Found Bones", + default=False + ) + show_non_standard: BoolProperty( + name="Show Non-Standard Bones", + default=False + ) + show_hierarchy: BoolProperty( + name="Show Hierarchy Issues", + default=False + ) diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 5f23be2..8b83eff 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -69,6 +69,15 @@ "Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}", "Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}", "Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists", + "Armature.validation.found_bones": "Found bones in armature:\n- {bones}", + "Armature.validation.non_standard_bones": "Non-standard bones found:\n- {bones}", + "Validation.section.found_bones": "Found Bones", + "Validation.section.non_standard": "Non-Standard Bones", + "Validation.section.hierarchy": "Hierarchy Issues", + "Validation.status.failed": "Validation has failed", + "Validation.message.failed.line1": "Armature validation has failed", + "Validation.message.failed.line2": "Please check below what the", + "Validation.message.failed.line3": "issues are", "Mesh.validation.no_data": "No mesh data", "Mesh.validation.no_vertex_groups": "No vertex groups found", diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index ca712e9..572081e 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -70,6 +70,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): def draw(self, context: Context) -> None: """Draw the panel layout""" layout: UILayout = self.layout + props = context.scene.avatar_toolkit # Armature Selection Box armature_box: UILayout = layout.box() @@ -83,26 +84,67 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): # Armature Validation active_armature: Optional[Object] = get_active_armature(context) if active_armature: - is_valid: bool - messages: List[str] is_valid, messages = validate_armature(active_armature) - # Create info box for all validation information - info_box: UILayout = col.box() + info_box = col.box() if is_valid: - row: UILayout = info_box.row() - split: UILayout = row.split(factor=0.6) + row = info_box.row() + split = row.split(factor=0.6) split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') - stats: Dict[str, int] = get_armature_stats(active_armature) + stats = get_armature_stats(active_armature) split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) if stats['has_pose']: info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') else: - # Display validation failure messages - for message in messages: - info_box.label(text=message, icon='ERROR') + # Found Bones section + validation_box = info_box.box() + row = validation_box.row() + row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False) + if props.show_found_bones: + for line in messages[0].split('\n'): + validation_box.label(text=line) + + # Main validation status + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.status.failed")) + + # Detailed validation message + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.message.failed.line1")) + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.message.failed.line2")) + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.message.failed.line3")) + + # Non-Standard Bones section + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"), icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False) + if props.show_non_standard: + for line in messages[1].split('\n'): + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=line) + + # Hierarchy Issues section + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"), icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False) + if props.show_hierarchy: + for message in messages[2:]: + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=message) # Validation Mode Warnings - always show in info box validation_mode = context.scene.avatar_toolkit.validation_mode From dd36ccaece64a56c1809e632c9f56f5091d2fde7 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 8 Feb 2025 11:03:22 +0000 Subject: [PATCH 4/5] Acceptable Standards Added --- core/armature_validation.py | 49 +++++++++++++++++--- core/dictionaries.py | 56 +++++++++++++++++++++++ core/properties.py | 14 +----- functions/custom_tools/mesh_attachment.py | 4 +- functions/pose_mode.py | 4 +- functions/tools/additional_tools.py | 8 ++-- functions/tools/bone_tools.py | 8 ++-- functions/tools/convert_resonite.py | 4 +- functions/tools/merge_tools.py | 4 +- functions/tools/mesh_separation.py | 8 ++-- functions/tools/rigify_converter.py | 3 +- functions/visemes.py | 5 +- ui/quick_access_panel.py | 33 +++++++++---- 13 files changed, 147 insertions(+), 53 deletions(-) diff --git a/core/armature_validation.py b/core/armature_validation.py index f32ef1b..cd74c5b 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -5,21 +5,29 @@ from ..core.translations import t from ..core.dictionaries import ( standard_bones, bone_hierarchy, - finger_hierarchy + finger_hierarchy, + acceptable_bone_hierarchy, + acceptable_bone_names ) -def validate_armature(armature: Object) -> Tuple[bool, List[str]]: +def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: + """ + Validates armature and returns (is_valid, messages, is_acceptable_standard) + """ validation_mode = bpy.context.scene.avatar_toolkit.validation_mode messages: List[str] = [] if validation_mode == 'NONE': - return True, [] + return True, [], False if not armature or armature.type != 'ARMATURE' or not armature.data.bones: - return False, [t("Armature.validation.basic_check_failed")] + return False, [t("Armature.validation.basic_check_failed")], False found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones} + # Check if armature matches acceptable standards + is_acceptable = check_acceptable_standards(found_bones) + # List all bones in armature bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()]) messages.append(t("Armature.validation.found_bones", bones=bone_list)) @@ -75,8 +83,17 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str]]: if not validate_finger_chain(found_bones, finger_chain): messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) - is_valid: bool = len(messages) == 0 - return is_valid, messages + is_valid = len(messages) == 0 + + if not is_valid and is_acceptable: + messages = [ + t("Armature.validation.acceptable_standard.success"), + t("Armature.validation.acceptable_standard.note"), + t("Armature.validation.acceptable_standard.option") + ] + return True, messages, True + + return is_valid, messages, False 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""" @@ -109,3 +126,23 @@ def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> boo if not validate_bone_hierarchy(bones, chain[i], chain[i + 1]): return False return True + +def check_acceptable_standards(bones: Dict[str, Bone]) -> bool: + """Check if armature matches acceptable non-standard hierarchy""" + # Check if bones exist in acceptable list + for bone_category, acceptable_names in acceptable_bone_names.items(): + found = False + for name in acceptable_names: + if name in bones: + found = True + break + if not found: + return False + + # Validate acceptable hierarchy + for parent, child in acceptable_bone_hierarchy: + if parent in bones and child in bones: + if not validate_bone_hierarchy(bones, parent, child): + return False + + return True diff --git a/core/dictionaries.py b/core/dictionaries.py index 573228a..ebcbb89 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -470,6 +470,62 @@ finger_hierarchy = { ] } +acceptable_bone_hierarchy = [ + # Right side chain + ('Hips', 'Chest'), + ('Chest', 'Shoulder.R'), + ('Shoulder.R', 'Arm.R'), + ('Arm.R', 'Elbow.R'), + ('Elbow.R', 'Wrist.R'), + ('Hips', 'Leg.R'), + ('Leg.R', 'Knee.R'), + ('Knee.R', 'Foot.R'), + ('Foot.R', 'Toes.R'), + + # Left side chain + ('Chest', 'Shoulder.L'), + ('Shoulder.L', 'Arm.L'), + ('Arm.L', 'Elbow.L'), + ('Elbow.L', 'Wrist.L'), + ('Hips', 'Leg.L'), + ('Leg.L', 'Knee.L'), + ('Knee.L', 'Foot.L'), + ('Foot.L', 'Toes.L'), + + # Head and Eyes + ('Chest', 'Neck'), + ('Neck', 'Head'), + ('Head', 'Eye_L'), + ('Head', 'Eye_R'), + ('Head', 'LeftEye'), + ('Head', 'RightEye') +] + +acceptable_bone_names = { + 'hips': ['Hips'], + 'chest': ['Chest'], + 'neck': ['Neck'], + 'head': ['Head'], + 'eye_l': ['Eye_L', 'LeftEye'], + 'eye_r': ['Eye_R', 'RightEye'], + 'shoulder_r': ['Shoulder.R'], + 'arm_r': ['Arm.R'], + 'elbow_r': ['Elbow.R'], + 'wrist_r': ['Wrist.R'], + 'leg_r': ['Leg.R'], + 'knee_r': ['Knee.R'], + 'foot_r': ['Foot.R'], + 'toes_r': ['Toes.R'], + 'shoulder_l': ['Shoulder.L'], + 'arm_l': ['Arm.L'], + 'elbow_l': ['Elbow.L'], + 'wrist_l': ['Wrist.L'], + 'leg_l': ['Leg.L'], + 'knee_l': ['Knee.L'], + 'foot_l': ['Foot.L'], + 'toes_l': ['Toes.L'] +} + rigify_unity_names = { "DEF-spine": "Hips", "DEF-spine.001": "Spine", diff --git a/core/properties.py b/core/properties.py index b944662..6b5f28d 100644 --- a/core/properties.py +++ b/core/properties.py @@ -396,12 +396,6 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=0 ) - merge_twist_bones: BoolProperty( - name=t("Tools.merge_twist_bones"), - description=t("Tools.merge_twist_bones_desc"), - default=True - ) - list_only_mode: BoolProperty( name=t("Tools.list_only_mode"), description=t("Tools.list_only_mode_desc"), @@ -521,19 +515,13 @@ class AvatarToolkitSceneProperties(PropertyGroup): default=False ) show_non_standard: BoolProperty( - name="Show Non-Standard Bones", + name="Show Non-Standard Bones", default=False ) show_hierarchy: BoolProperty( name="Show Hierarchy Issues", default=False ) - - merge_twist_bones: BoolProperty( - name=t("Tools.merge_twist_bones"), - description=t("Tools.merge_twist_bones_desc"), - default=True - ) def register() -> None: """Register the Avatar Toolkit property group""" diff --git a/functions/custom_tools/mesh_attachment.py b/functions/custom_tools/mesh_attachment.py index 1c20a97..d660e76 100644 --- a/functions/custom_tools/mesh_attachment.py +++ b/functions/custom_tools/mesh_attachment.py @@ -27,8 +27,8 @@ class AvatarToolkit_OT_AttachMesh(Operator): armature: Optional[Object] = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> Set[str]: try: diff --git a/functions/pose_mode.py b/functions/pose_mode.py index 0c57f2e..6cf2b00 100644 --- a/functions/pose_mode.py +++ b/functions/pose_mode.py @@ -23,7 +23,7 @@ class BatchPoseOperationMixin: armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid and context.mode == 'POSE' def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]: @@ -46,7 +46,7 @@ class AvatarToolkit_OT_StartPoseMode(Operator): armature = get_active_armature(context) if not armature or context.mode == "POSE": return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid def execute(self, context: Context) -> Set[str]: diff --git a/functions/tools/additional_tools.py b/functions/tools/additional_tools.py index 7cee04c..91afaee 100644 --- a/functions/tools/additional_tools.py +++ b/functions/tools/additional_tools.py @@ -19,8 +19,8 @@ class AvatarToolkit_OT_ApplyTransforms(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid and context.mode == 'OBJECT' + valid, _, _ = validate_armature(armature) + return valid and context.mode == 'OBJECT' def execute(self, context: Context) -> Set[str]: try: @@ -67,8 +67,8 @@ class AvatarToolkit_OT_CleanShapekeys(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0 + valid, _, _ = validate_armature(armature) + return valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0 def execute(self, context: Context) -> Set[str]: try: diff --git a/functions/tools/bone_tools.py b/functions/tools/bone_tools.py index 26e0873..78e6c72 100644 --- a/functions/tools/bone_tools.py +++ b/functions/tools/bone_tools.py @@ -34,8 +34,8 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return (is_valid and + valid, _, _ = validate_armature(armature) + return (valid and context.mode == 'EDIT_ARMATURE' and context.selected_editable_bones is not None and len(context.selected_editable_bones) == 2) @@ -128,8 +128,8 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> set[str]: """Execute the constraint removal operation""" diff --git a/functions/tools/convert_resonite.py b/functions/tools/convert_resonite.py index be5d72d..a41678a 100644 --- a/functions/tools/convert_resonite.py +++ b/functions/tools/convert_resonite.py @@ -20,8 +20,8 @@ class AvatarToolkit_OT_ConvertResonite(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> Set[str]: armature = get_active_armature(context) diff --git a/functions/tools/merge_tools.py b/functions/tools/merge_tools.py index d5c5426..4078f91 100644 --- a/functions/tools/merge_tools.py +++ b/functions/tools/merge_tools.py @@ -19,8 +19,8 @@ class AvatarToolkit_OT_ConnectBones(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) - return is_valid + valid, _, _ = validate_armature(armature) + return valid def execute(self, context: Context) -> Set[str]: try: diff --git a/functions/tools/mesh_separation.py b/functions/tools/mesh_separation.py index 78c58d9..96d8881 100644 --- a/functions/tools/mesh_separation.py +++ b/functions/tools/mesh_separation.py @@ -17,10 +17,10 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return (context.active_object and context.active_object.type == 'MESH' and - is_valid) + valid) def execute(self, context: Context) -> set[str]: """Execute the separation operation""" @@ -49,10 +49,10 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator): armature = get_active_armature(context) if not armature: return False - is_valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return (context.active_object and context.active_object.type == 'MESH' and - is_valid) + valid) def execute(self, context: Context) -> set[str]: """Execute the separation operation""" diff --git a/functions/tools/rigify_converter.py b/functions/tools/rigify_converter.py index b04bd89..8737454 100644 --- a/functions/tools/rigify_converter.py +++ b/functions/tools/rigify_converter.py @@ -1,10 +1,11 @@ import bpy from typing import Dict, List, Set, Optional, Tuple, Any from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint -from ...core.common import get_active_armature, validate_armature +from ...core.common import get_active_armature from ...core.logging_setup import logger from ...core.translations import t from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones +from ...core.armature_validation import validate_armature class AvatarToolkit_OT_ConvertRigifyToUnity(Operator): """Convert Rigify armature to Unity-compatible format""" diff --git a/functions/visemes.py b/functions/visemes.py index d396150..3492f0e 100644 --- a/functions/visemes.py +++ b/functions/visemes.py @@ -138,9 +138,8 @@ class ATOOLKIT_OT_preview_visemes(Operator): armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid and mesh_obj and mesh_obj.type == 'MESH' - def execute(self, context: Context) -> Set[str]: props = context.scene.avatar_toolkit @@ -197,7 +196,7 @@ class ATOOLKIT_OT_create_visemes(Operator): armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid and mesh_obj and mesh_obj.type == 'MESH' diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index 572081e..d20db3a 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -84,19 +84,31 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): # Armature Validation active_armature: Optional[Object] = get_active_armature(context) if active_armature: - is_valid, messages = validate_armature(active_armature) + is_valid, messages, is_acceptable = validate_armature(active_armature) info_box = col.box() if is_valid: - row = info_box.row() - split = row.split(factor=0.6) - split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') - stats = get_armature_stats(active_armature) - split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) - - if stats['has_pose']: - info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') + if is_acceptable: + # Show acceptable standard message + info_box.label(text=messages[0], icon='INFO') + info_box.label(text=messages[1]) + info_box.label(text=messages[2]) + + # Add standardize button + standardize_box = info_box.box() + standardize_box.operator("avatar_toolkit.standardize_armature", + text=t("QuickAccess.standardize_armature"), + icon='MODIFIER') + else: + row = info_box.row() + split = row.split(factor=0.6) + split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') + stats = get_armature_stats(active_armature) + split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) + + if stats['has_pose']: + info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') else: # Found Bones section validation_box = info_box.box() @@ -146,7 +158,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): sub_row.alert = True sub_row.label(text=message) - # Validation Mode Warnings - always show in info box + # Validation Mode Warnings validation_mode = context.scene.avatar_toolkit.validation_mode if validation_mode == 'BASIC': warning_row = info_box.box() @@ -184,3 +196,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): button_row.scale_y = 1.5 button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT') button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT') + From fbb07aec10a5284ff00444028a567ca63229b242 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Sat, 8 Feb 2025 14:18:53 +0000 Subject: [PATCH 5/5] Fixes - Fixed issue with plugin registration - Fixed bones symmetry not working - Fixed different modes not working - Some other small fixes. --- core/armature_validation.py | 107 ++++++----- core/properties.py | 220 +++++++++++----------- functions/optimization/materials_tools.py | 2 +- functions/optimization/mesh_tools.py | 4 +- functions/optimization/remove_doubles.py | 4 +- ui/quick_access_panel.py | 135 ++++++------- 6 files changed, 249 insertions(+), 223 deletions(-) diff --git a/core/armature_validation.py b/core/armature_validation.py index cd74c5b..3bc193b 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -16,6 +16,8 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: """ validation_mode = bpy.context.scene.avatar_toolkit.validation_mode messages: List[str] = [] + hierarchy_messages: List[str] = [] + non_standard_messages: List[str] = [] if validation_mode == 'NONE': return True, [], False @@ -24,68 +26,76 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: return False, [t("Armature.validation.basic_check_failed")], False found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones} - - # Check if armature matches acceptable standards is_acceptable = check_acceptable_standards(found_bones) # List all bones in armature bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()]) messages.append(t("Armature.validation.found_bones", bones=bone_list)) - # Check each bone against our standard - non_standard_bones = [] - required_patterns = [ - 'Hips', 'Spine', 'Chest', 'Neck', 'Head', - 'Upper', 'Lower', 'Hand', 'Foot', 'Toe', - 'Thumb', 'Index', 'Middle', 'Ring', 'Pinky', - 'Eye' - ] - - for bone_name in found_bones: - if any(pattern in bone_name for pattern in required_patterns): - if bone_name not in standard_bones.values(): - non_standard_bones.append(bone_name) - - if non_standard_bones: - non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones]) - messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list)) - + # Basic validation for both STRICT and LIMITED modes # Check for missing required bones essential_bones = {standard_bones[key] for key in ['hips', 'spine', 'chest', 'neck', 'head']} missing_bones = [bone for bone in essential_bones if bone not in found_bones] if missing_bones: missing_list = "\n".join([f"- {bone}" for bone in missing_bones]) - messages.append(t("Armature.validation.missing_bones", bones=missing_list)) - + hierarchy_messages.append(t("Armature.validation.missing_bones", bones=missing_list)) + if validation_mode == 'STRICT': # Validate bone hierarchy for parent, child in bone_hierarchy: if parent in found_bones and child in found_bones: if not validate_bone_hierarchy(found_bones, parent, child): - messages.append(t("Armature.validation.invalid_hierarchy", + hierarchy_messages.append(t("Armature.validation.invalid_hierarchy", parent=parent, child=child)) # Validate symmetry - symmetry_pairs = [('arm', 'l', 'r'), ('leg', 'l', 'r')] + symmetry_pairs = [('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)) + hierarchy_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")) - + if (not validate_symmetry(found_bones, 'hand', 'L', 'R') and + not validate_symmetry(found_bones, 'wrist', 'L', 'R')): + hierarchy_messages.append(t("Armature.validation.asymmetric_hand_wrist")) + # Validate finger hierarchies for side in ['left', 'right']: for finger_chain in finger_hierarchy[side]: if all(bone in found_bones for bone in finger_chain): if not validate_finger_chain(found_bones, finger_chain): - messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) + hierarchy_messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) + + # Non-standard bones check + non_standard_bones = [] + required_patterns = [ + 'Hips', 'Spine', 'Chest', 'Neck', 'Head', + 'Upper', 'Lower', 'Hand', 'Foot', 'Toe', + 'Thumb', 'Index', 'Middle', 'Ring', 'Pinky', + 'Eye' + ] + + for bone_name in found_bones: + if any(pattern in bone_name for pattern in required_patterns): + is_standard = bone_name in standard_bones.values() + is_acceptable_bone = any(bone_name in names for names in acceptable_bone_names.values()) + if not (is_standard or is_acceptable_bone): + non_standard_bones.append(bone_name) + + if non_standard_bones: + non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones]) + non_standard_messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list)) - is_valid = len(messages) == 0 + # Combine messages in correct order + messages.extend(non_standard_messages) + messages.extend(hierarchy_messages) + + is_valid = len(non_standard_messages) == 0 and len(hierarchy_messages) == 0 if not is_valid and is_acceptable: + if non_standard_bones: + return False, messages, False + messages = [ t("Armature.validation.acceptable_standard.success"), t("Armature.validation.acceptable_standard.note"), @@ -103,22 +113,31 @@ def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name 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}" - ] + # Extract left and right bone names from both hierarchies + left_bone_names = set() + right_bone_names = set() - right_patterns: List[str] = [ - f"{base}.{right}", - f"{base}_{right}", - f"{right}_{base}" - ] + # Add standard bones + for key, value in standard_bones.items(): + if base in key.lower(): + if '_l' in key.lower(): + left_bone_names.add(value) + elif '_r' in key.lower(): + right_bone_names.add(value) + + # Add acceptable bones + for key, names in acceptable_bone_names.items(): + if base in key.lower(): + if '_l' in key.lower(): + left_bone_names.update(names) + elif '_r' in key.lower(): + right_bone_names.update(names) - left_exists: bool = any(pattern in bones for pattern in left_patterns) - right_exists: bool = any(pattern in bones for pattern in right_patterns) + # Check if at least one pair exists and matches + left_exists = any(name in bones for name in left_bone_names) + right_exists = any(name in bones for name in right_bone_names) - return left_exists and right_exists + return left_exists == right_exists def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> bool: """Validate if a finger bone chain has correct hierarchy""" diff --git a/core/properties.py b/core/properties.py index 6b5f28d..fa1b0b5 100644 --- a/core/properties.py +++ b/core/properties.py @@ -44,6 +44,117 @@ def update_shape_intensity(self: PropertyGroup, context: Context) -> None: class AvatarToolkitSceneProperties(PropertyGroup): """Property group containing Avatar Toolkit scene-level settings and properties""" + + show_found_bones: BoolProperty( + name="Show Found Bones", + default=False + ) + + show_non_standard: BoolProperty( + name="Show Non-Standard Bones", + default=False + ) + + show_hierarchy: BoolProperty( + name="Show Hierarchy Issues", + default=False + ) + + material_search_filter: StringProperty( + name=t("TextureAtlas.search_materials"), + description=t("TextureAtlas.search_materials_desc"), + default="" + ) + + def get_texture_node_list(self: Material, context: Context) -> list[tuple]: + if self.use_nodes: + Object.Enum = [((i.image.name if i.image else i.name+"_image"), + (i.image.name if i.image else "node with no image..."), + (i.image.name if i.image else i.name),index+1) + for index,i in enumerate(self.node_tree.nodes) + if i.bl_idname == "ShaderNodeTexImage"] + if not len(Object.Enum): + Object.Enum = [(t("TextureAtlas.error.label"), + t("TextureAtlas.no_images_error.desc"), + t("TextureAtlas.error.label"), 0)] + else: + Object.Enum = [(t("TextureAtlas.error.label"), + t("TextureAtlas.no_nodes_error.desc"), + t("TextureAtlas.error.label"), 0)] + Object.Enum.append((t("TextureAtlas.none.label"), + t("TextureAtlas.none.label"), + t("TextureAtlas.none.label"), 0)) + return Object.Enum + + Material.texture_atlas_albedo = EnumProperty( + name=t("TextureAtlas.albedo"), + description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()), + default=0, + items=get_texture_node_list + ) + + Material.texture_atlas_normal = EnumProperty( + name=t("TextureAtlas.normal"), + description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()), + default=0, + items=get_texture_node_list + ) + + Material.texture_atlas_emission = EnumProperty( + name=t("TextureAtlas.emission"), + description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()), + default=0, + items=get_texture_node_list + ) + + Material.texture_atlas_ambient_occlusion = EnumProperty( + name=t("TextureAtlas.ambient_occlusion"), + description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()), + default=0, + items=get_texture_node_list + ) + + Material.texture_atlas_height = EnumProperty( + name=t("TextureAtlas.height"), + description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()), + default=0, + items=get_texture_node_list + ) + + Material.texture_atlas_roughness = EnumProperty( + name=t("TextureAtlas.roughness"), + description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()), + default=0, + items=get_texture_node_list + ) + + Material.include_in_atlas = BoolProperty( + name=t("TextureAtlas.include_in_atlas"), + description=t("TextureAtlas.include_in_atlas_desc"), + default=False + ) + + Material.material_expanded = BoolProperty( + name=t("TextureAtlas.material_expanded"), + description=t("TextureAtlas.material_expanded_desc"), + default=False + ) + + texture_atlas_Has_Mat_List_Shown: BoolProperty( + name=t("TextureAtlas.list_shown"), + description=t("TextureAtlas.list_shown_desc"), + default=False + ) + + texture_atlas_material_index: IntProperty( + default=-1, + get=lambda self: -1, + set=lambda self, context: None + ) + + materials: CollectionProperty( + type=SceneMatClass + ) avatar_toolkit_updater_version_list: EnumProperty( items=get_version_list, @@ -407,121 +518,12 @@ class AvatarToolkitSceneProperties(PropertyGroup): description=t('MergeArmature.cleanup_shape_keys_desc'), default=True ) - - material_search_filter: StringProperty( - name=t("TextureAtlas.search_materials"), - description=t("TextureAtlas.search_materials_desc"), - default="" - ) - - def get_texture_node_list(self: Material, context: Context) -> list[tuple]: - if self.use_nodes: - Object.Enum = [((i.image.name if i.image else i.name+"_image"), - (i.image.name if i.image else "node with no image..."), - (i.image.name if i.image else i.name),index+1) - for index,i in enumerate(self.node_tree.nodes) - if i.bl_idname == "ShaderNodeTexImage"] - if not len(Object.Enum): - Object.Enum = [(t("TextureAtlas.error.label"), - t("TextureAtlas.no_images_error.desc"), - t("TextureAtlas.error.label"), 0)] - else: - Object.Enum = [(t("TextureAtlas.error.label"), - t("TextureAtlas.no_nodes_error.desc"), - t("TextureAtlas.error.label"), 0)] - Object.Enum.append((t("TextureAtlas.none.label"), - t("TextureAtlas.none.label"), - t("TextureAtlas.none.label"), 0)) - return Object.Enum - - Material.texture_atlas_albedo = EnumProperty( - name=t("TextureAtlas.albedo"), - description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.albedo").lower()), - default=0, - items=get_texture_node_list - ) - - Material.texture_atlas_normal = EnumProperty( - name=t("TextureAtlas.normal"), - description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.normal").lower()), - default=0, - items=get_texture_node_list - ) - - Material.texture_atlas_emission = EnumProperty( - name=t("TextureAtlas.emission"), - description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.emission").lower()), - default=0, - items=get_texture_node_list - ) - - Material.texture_atlas_ambient_occlusion = EnumProperty( - name=t("TextureAtlas.ambient_occlusion"), - description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.ambient_occlusion").lower()), - default=0, - items=get_texture_node_list - ) - - Material.texture_atlas_height = EnumProperty( - name=t("TextureAtlas.height"), - description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.height").lower()), - default=0, - items=get_texture_node_list - ) - - Material.texture_atlas_roughness = EnumProperty( - name=t("TextureAtlas.roughness"), - description=t("TextureAtlas.texture_use_atlas.desc").format(name=t("TextureAtlas.roughness").lower()), - default=0, - items=get_texture_node_list - ) - - Material.include_in_atlas = BoolProperty( - name=t("TextureAtlas.include_in_atlas"), - description=t("TextureAtlas.include_in_atlas_desc"), - default=False - ) - - Material.material_expanded = BoolProperty( - name=t("TextureAtlas.material_expanded"), - description=t("TextureAtlas.material_expanded_desc"), - default=False - ) - - texture_atlas_Has_Mat_List_Shown: BoolProperty( - name=t("TextureAtlas.list_shown"), - description=t("TextureAtlas.list_shown_desc"), - default=False - ) - - texture_atlas_material_index: IntProperty( - default=-1, - get=lambda self: -1, - set=lambda self, context: None - ) - - materials: CollectionProperty( - type=SceneMatClass - ) merge_twist_bones: BoolProperty( name=t("Tools.merge_twist_bones"), description=t("Tools.merge_twist_bones_desc"), default=True ) - - show_found_bones: BoolProperty( - name="Show Found Bones", - default=False - ) - show_non_standard: BoolProperty( - name="Show Non-Standard Bones", - default=False - ) - show_hierarchy: BoolProperty( - name="Show Hierarchy Issues", - default=False - ) def register() -> None: """Register the Avatar Toolkit property group""" diff --git a/functions/optimization/materials_tools.py b/functions/optimization/materials_tools.py index 95f54f8..b6983d4 100644 --- a/functions/optimization/materials_tools.py +++ b/functions/optimization/materials_tools.py @@ -92,7 +92,7 @@ class AvatarToolkit_OT_CombineMaterials(Operator): armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid def execute(self, context: Context) -> Set[str]: diff --git a/functions/optimization/mesh_tools.py b/functions/optimization/mesh_tools.py index 19a23c4..825b493 100644 --- a/functions/optimization/mesh_tools.py +++ b/functions/optimization/mesh_tools.py @@ -25,7 +25,7 @@ class AvatarToolkit_OT_JoinAllMeshes(Operator): if not armature: return False valid: bool - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid def execute(self, context: Context) -> Set[str]: @@ -69,7 +69,7 @@ class AvatarToolkit_OT_JoinSelectedMeshes(Operator): if not armature: return False valid: bool - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return (valid and context.mode == 'OBJECT' and len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1) diff --git a/functions/optimization/remove_doubles.py b/functions/optimization/remove_doubles.py index ef8c608..e5c20e5 100644 --- a/functions/optimization/remove_doubles.py +++ b/functions/optimization/remove_doubles.py @@ -88,7 +88,7 @@ class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator): armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid def execute(self, context: Context) -> set[str]: @@ -111,7 +111,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator): armature = get_active_armature(context) if not armature: return False - valid, _ = validate_armature(armature) + valid, _, _ = validate_armature(armature) return valid def draw(self, context: Context) -> None: diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index d20db3a..0d749e6 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -88,75 +88,81 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): info_box = col.box() - if is_valid: - if is_acceptable: - # Show acceptable standard message + if not is_valid: + # Display non-standard bones and hierarchy issues + if len(messages) > 1: + # Found Bones section + validation_box = info_box.box() + row = validation_box.row() + row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False) + if props.show_found_bones: + for line in messages[0].split('\n'): + validation_box.label(text=line) + + # Main validation status + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.status.failed")) + + # Detailed validation message + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.message.failed.line1")) + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.message.failed.line2")) + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.message.failed.line3")) + + # Non-Standard Bones section + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"), icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False) + if props.show_non_standard: + for line in messages[1].split('\n'): + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=line) + + # Hierarchy Issues section + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"), icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False) + if props.show_hierarchy: + for message in messages[2:]: + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=message) + else: + # If no specific issues, show acceptable message info_box.label(text=messages[0], icon='INFO') info_box.label(text=messages[1]) info_box.label(text=messages[2]) - - # Add standardize button - standardize_box = info_box.box() - standardize_box.operator("avatar_toolkit.standardize_armature", - text=t("QuickAccess.standardize_armature"), - icon='MODIFIER') - else: - row = info_box.row() - split = row.split(factor=0.6) - split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') - stats = get_armature_stats(active_armature) - split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) - - if stats['has_pose']: - info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') - else: - # Found Bones section - validation_box = info_box.box() - row = validation_box.row() - row.prop(props, "show_found_bones", text=t("Validation.section.found_bones"), icon='TRIA_DOWN' if props.show_found_bones else 'TRIA_RIGHT', emboss=False) - if props.show_found_bones: - for line in messages[0].split('\n'): - validation_box.label(text=line) + elif is_valid and not is_acceptable: + row = info_box.row() + split = row.split(factor=0.6) + split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') + stats = get_armature_stats(active_armature) + split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) - # Main validation status - validation_box = info_box.box() - row = validation_box.row() - row.alert = True - row.label(text=t("Validation.status.failed")) + if stats['has_pose']: + info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') + elif is_valid and is_acceptable: + # Show acceptable standard message + info_box.label(text=messages[0], icon='INFO') + info_box.label(text=messages[1]) + info_box.label(text=messages[2]) - # Detailed validation message - validation_box = info_box.box() - row = validation_box.row() - row.alert = True - row.label(text=t("Validation.message.failed.line1")) - row = validation_box.row() - row.alert = True - row.label(text=t("Validation.message.failed.line2")) - row = validation_box.row() - row.alert = True - row.label(text=t("Validation.message.failed.line3")) - - # Non-Standard Bones section - validation_box = info_box.box() - row = validation_box.row() - row.alert = True - row.prop(props, "show_non_standard", text=t("Validation.section.non_standard"), icon='TRIA_DOWN' if props.show_non_standard else 'TRIA_RIGHT', emboss=False) - if props.show_non_standard: - for line in messages[1].split('\n'): - sub_row = validation_box.row() - sub_row.alert = True - sub_row.label(text=line) - - # Hierarchy Issues section - validation_box = info_box.box() - row = validation_box.row() - row.alert = True - row.prop(props, "show_hierarchy", text=t("Validation.section.hierarchy"), icon='TRIA_DOWN' if props.show_hierarchy else 'TRIA_RIGHT', emboss=False) - if props.show_hierarchy: - for message in messages[2:]: - sub_row = validation_box.row() - sub_row.alert = True - sub_row.label(text=message) + # Add standardize button + standardize_box = info_box.box() + standardize_box.operator("avatar_toolkit.standardize_armature", + text=t("QuickAccess.standardize_armature"), + icon='MODIFIER') # Validation Mode Warnings validation_mode = context.scene.avatar_toolkit.validation_mode @@ -196,4 +202,3 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): button_row.scale_y = 1.5 button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT') button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT') -