Acceptable Standards Added
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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]:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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"""
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user