Acceptable Standards Added

This commit is contained in:
Yusarina
2025-02-08 11:03:22 +00:00
parent 017633696a
commit dd36ccaece
13 changed files with 147 additions and 53 deletions
+43 -6
View File
@@ -5,21 +5,29 @@ from ..core.translations import t
from ..core.dictionaries import ( from ..core.dictionaries import (
standard_bones, standard_bones,
bone_hierarchy, 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 validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
messages: List[str] = [] messages: List[str] = []
if validation_mode == 'NONE': if validation_mode == 'NONE':
return True, [] return True, [], False
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")], False
found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones} 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 # List all bones in armature
bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()]) bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()])
messages.append(t("Armature.validation.found_bones", bones=bone_list)) 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): if not validate_finger_chain(found_bones, finger_chain):
messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0]))
is_valid: bool = len(messages) == 0 is_valid = len(messages) == 0
return is_valid, messages
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: 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"""
@@ -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]): if not validate_bone_hierarchy(bones, chain[i], chain[i + 1]):
return False return False
return True 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
+56
View File
@@ -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 = { rigify_unity_names = {
"DEF-spine": "Hips", "DEF-spine": "Hips",
"DEF-spine.001": "Spine", "DEF-spine.001": "Spine",
-12
View File
@@ -396,12 +396,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=0 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( list_only_mode: BoolProperty(
name=t("Tools.list_only_mode"), name=t("Tools.list_only_mode"),
description=t("Tools.list_only_mode_desc"), description=t("Tools.list_only_mode_desc"),
@@ -529,12 +523,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=False default=False
) )
merge_twist_bones: BoolProperty(
name=t("Tools.merge_twist_bones"),
description=t("Tools.merge_twist_bones_desc"),
default=True
)
def register() -> None: def register() -> None:
"""Register the Avatar Toolkit property group""" """Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties") logger.info("Registering Avatar Toolkit properties")
+2 -2
View File
@@ -27,8 +27,8 @@ class AvatarToolkit_OT_AttachMesh(Operator):
armature: Optional[Object] = get_active_armature(context) armature: Optional[Object] = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return is_valid return valid
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
try: try:
+2 -2
View File
@@ -23,7 +23,7 @@ class BatchPoseOperationMixin:
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return valid and context.mode == 'POSE' return valid and context.mode == 'POSE'
def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]: 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) armature = get_active_armature(context)
if not armature or context.mode == "POSE": if not armature or context.mode == "POSE":
return False return False
valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return valid return valid
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
+4 -4
View File
@@ -19,8 +19,8 @@ class AvatarToolkit_OT_ApplyTransforms(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT' return valid and context.mode == 'OBJECT'
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
try: try:
@@ -67,8 +67,8 @@ class AvatarToolkit_OT_CleanShapekeys(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0 return valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
try: try:
+4 -4
View File
@@ -34,8 +34,8 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return (is_valid and return (valid and
context.mode == 'EDIT_ARMATURE' and context.mode == 'EDIT_ARMATURE' and
context.selected_editable_bones is not None and context.selected_editable_bones is not None and
len(context.selected_editable_bones) == 2) len(context.selected_editable_bones) == 2)
@@ -128,8 +128,8 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return is_valid return valid
def execute(self, context: Context) -> set[str]: def execute(self, context: Context) -> set[str]:
"""Execute the constraint removal operation""" """Execute the constraint removal operation"""
+2 -2
View File
@@ -20,8 +20,8 @@ class AvatarToolkit_OT_ConvertResonite(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return is_valid return valid
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context) armature = get_active_armature(context)
+2 -2
View File
@@ -19,8 +19,8 @@ class AvatarToolkit_OT_ConnectBones(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return is_valid return valid
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
try: try:
+4 -4
View File
@@ -17,10 +17,10 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return (context.active_object and return (context.active_object and
context.active_object.type == 'MESH' and context.active_object.type == 'MESH' and
is_valid) valid)
def execute(self, context: Context) -> set[str]: def execute(self, context: Context) -> set[str]:
"""Execute the separation operation""" """Execute the separation operation"""
@@ -49,10 +49,10 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
is_valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return (context.active_object and return (context.active_object and
context.active_object.type == 'MESH' and context.active_object.type == 'MESH' and
is_valid) valid)
def execute(self, context: Context) -> set[str]: def execute(self, context: Context) -> set[str]:
"""Execute the separation operation""" """Execute the separation operation"""
+2 -1
View File
@@ -1,10 +1,11 @@
import bpy import bpy
from typing import Dict, List, Set, Optional, Tuple, Any from typing import Dict, List, Set, Optional, Tuple, Any
from bpy.types import Operator, Context, Object, PoseBone, EditBone, Bone, Constraint 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.logging_setup import logger
from ...core.translations import t from ...core.translations import t
from ...core.dictionaries import rigify_unity_names, rigify_basic_unity_names, rigify_unnecessary_bones 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): class AvatarToolkit_OT_ConvertRigifyToUnity(Operator):
"""Convert Rigify armature to Unity-compatible format""" """Convert Rigify armature to Unity-compatible format"""
+2 -3
View File
@@ -138,10 +138,9 @@ class ATOOLKIT_OT_preview_visemes(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return valid and mesh_obj and mesh_obj.type == 'MESH' return valid and mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit props = context.scene.avatar_toolkit
mesh = context.active_object mesh = context.active_object
@@ -197,7 +196,7 @@ class ATOOLKIT_OT_create_visemes(Operator):
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature: if not armature:
return False return False
valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return valid and mesh_obj and mesh_obj.type == 'MESH' return valid and mesh_obj and mesh_obj.type == 'MESH'
+15 -2
View File
@@ -84,11 +84,23 @@ 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, messages = validate_armature(active_armature) is_valid, messages, is_acceptable = validate_armature(active_armature)
info_box = col.box() info_box = col.box()
if is_valid: if is_valid:
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() row = info_box.row()
split = 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')
@@ -146,7 +158,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
sub_row.alert = True sub_row.alert = True
sub_row.label(text=message) sub_row.label(text=message)
# Validation Mode Warnings - always show in info box # Validation Mode Warnings
validation_mode = context.scene.avatar_toolkit.validation_mode validation_mode = context.scene.avatar_toolkit.validation_mode
if validation_mode == 'BASIC': if validation_mode == 'BASIC':
warning_row = info_box.box() warning_row = info_box.box()
@@ -184,3 +196,4 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
button_row.scale_y = 1.5 button_row.scale_y = 1.5
button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT') button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT')
button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT') button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT')