From 6412b6f619f8ba9762091b7a768dca1f6200c4c3 Mon Sep 17 00:00:00 2001 From: Yusarina Date: Fri, 7 Feb 2025 18:18:09 +0000 Subject: [PATCH] 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