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.
This commit is contained in:
Yusarina
2025-02-07 18:18:09 +00:00
parent 4b59147649
commit 6412b6f619
5 changed files with 250 additions and 45 deletions
+58 -35
View File
@@ -1,11 +1,14 @@
import bpy import bpy
from typing import Tuple, List, Dict, Set from typing import Tuple, List, Dict, Set, Optional
from bpy.types import Object, Bone from bpy.types import Object, Bone
from ..core.translations import t 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]]: 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 validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = [] 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: 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")]
found_bones: Dict[str, Bone] = {bone.name.lower(): bone for bone in armature.data.bones} found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones}
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones: List[str] = [] # List all bones in armature
for bone in essential_bones: bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()])
if not any(alt_name in found_bones for alt_name in bone_names[bone]): messages.append(t("Armature.validation.found_bones", bones=bone_list))
missing_bones.append(bone)
# 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: 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': if validation_mode == 'STRICT':
hierarchy: List[Tuple[str, str]] = [ # Validate bone hierarchy
('hips', 'spine'), ('spine', 'chest'), for parent, child in bone_hierarchy:
('chest', 'neck'), ('neck', 'head') if parent in found_bones and child in found_bones:
] if not validate_bone_hierarchy(found_bones, parent, child):
for parent, child in hierarchy: messages.append(t("Armature.validation.invalid_hierarchy",
if not validate_bone_hierarchy(found_bones, parent, child): parent=parent, child=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: for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right): if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base)) 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 if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')): not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist")) 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 is_valid: bool = len(messages) == 0
return is_valid, messages return is_valid, messages
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool: 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""" """Validate if there is a valid parent-child relationship between bones"""
parent_bone: Optional[Bone] = None if parent_name not in bones or child_name not in bones:
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 False
return bones[child_name].parent == bones[parent_name]
return child_bone.parent == parent_bone
def validate_symmetry(bones: Dict[str, Bone], base: str, left: str, right: str) -> bool: 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""" """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) right_exists: bool = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists 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
+118
View File
@@ -354,3 +354,121 @@ resonite_translations = {
'thumb_2_r': "thumb2.R", 'thumb_2_r': "thumb2.R",
'thumb_3_r': "thumb3.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')
]
}
+13
View File
@@ -462,6 +462,19 @@ class AvatarToolkitSceneProperties(PropertyGroup):
materials: CollectionProperty( materials: CollectionProperty(
type=SceneMatClass 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
)
+9
View File
@@ -69,6 +69,15 @@
"Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}", "Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}",
"Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}", "Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}",
"Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists", "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_data": "No mesh data",
"Mesh.validation.no_vertex_groups": "No vertex groups found", "Mesh.validation.no_vertex_groups": "No vertex groups found",
+52 -10
View File
@@ -70,6 +70,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
"""Draw the panel layout""" """Draw the panel layout"""
layout: UILayout = self.layout layout: UILayout = self.layout
props = context.scene.avatar_toolkit
# Armature Selection Box # Armature Selection Box
armature_box: UILayout = layout.box() armature_box: UILayout = layout.box()
@@ -83,26 +84,67 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
# Armature Validation # Armature Validation
active_armature: Optional[Object] = get_active_armature(context) active_armature: Optional[Object] = get_active_armature(context)
if active_armature: if active_armature:
is_valid: bool
messages: List[str]
is_valid, messages = validate_armature(active_armature) is_valid, messages = validate_armature(active_armature)
# Create info box for all validation information info_box = col.box()
info_box: UILayout = col.box()
if is_valid: if is_valid:
row: UILayout = info_box.row() row = info_box.row()
split: UILayout = row.split(factor=0.6) split = row.split(factor=0.6)
split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') 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'])) split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
if stats['has_pose']: if stats['has_pose']:
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
else: else:
# Display validation failure messages # Found Bones section
for message in messages: validation_box = info_box.box()
info_box.label(text=message, icon='ERROR') 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 Warnings - always show in info box
validation_mode = context.scene.avatar_toolkit.validation_mode validation_mode = context.scene.avatar_toolkit.validation_mode