Merge pull request #117 from Yusarina/Armature_Validation_Version2

Armature validation version 2 Part 1 (Don't merge yet)
This commit is contained in:
Onan Chew
2025-02-08 11:39:10 -05:00
committed by GitHub
22 changed files with 590 additions and 250 deletions
+167
View File
@@ -0,0 +1,167 @@
import bpy
from typing import Tuple, List, Dict, Set, Optional
from bpy.types import Object, Bone
from ..core.translations import t
from ..core.dictionaries import (
standard_bones,
bone_hierarchy,
finger_hierarchy,
acceptable_bone_hierarchy,
acceptable_bone_names
)
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] = []
hierarchy_messages: List[str] = []
non_standard_messages: List[str] = []
if validation_mode == 'NONE':
return True, [], False
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")], False
found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones}
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))
# 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])
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):
hierarchy_messages.append(t("Armature.validation.invalid_hierarchy",
parent=parent, child=child))
# 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):
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')):
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):
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))
# 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"),
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"""
if parent_name not in bones or child_name not in bones:
return False
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"""
# Extract left and right bone names from both hierarchies
left_bone_names = set()
right_bone_names = set()
# 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)
# 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 == 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
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
-86
View File
@@ -28,7 +28,6 @@ class MaterialListBool:
#For the love that is holy do not ever touch these. If this was java I would make these private #For the love that is holy do not ever touch these. If this was java I would make these private
#They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown #They should only be accessed via context.scene.texture_atlas_Has_Mat_List_Shown
#This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up. #This is so we know if the materials are up to date. messing with these variables directly will make the thing blow up.
#The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading. #The only exception to this is the ExpandSection_Materials operator which populates this with new data once the materials have changed and need reloading.
old_list: dict[str,list[Material]] = {} old_list: dict[str,list[Material]] = {}
bool_material_list_expand: dict[str,bool] = {} bool_material_list_expand: dict[str,bool] = {}
@@ -46,7 +45,6 @@ class MaterialListBool:
if mat_slot.material: if mat_slot.material:
if mat_slot.material not in newlist: if mat_slot.material not in newlist:
newlist.append(mat_slot.material) newlist.append(mat_slot.material)
still_the_same: bool = True still_the_same: bool = True
if bpy.context.scene.name in MaterialListBool.old_list: if bpy.context.scene.name in MaterialListBool.old_list:
for item in newlist: for item in newlist:
@@ -60,7 +58,6 @@ class MaterialListBool:
else: else:
still_the_same = False still_the_same = False
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name] return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
class ProgressTracker: class ProgressTracker:
@@ -110,89 +107,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
if not armatures: if not armatures:
return [('NONE', t("Armature.validation.no_armature"), '')] return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures 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: def auto_select_single_armature(context: Context) -> None:
"""Automatically select armature if only one exists in scene""" """Automatically select armature if only one exists in scene"""
+173 -1
View File
@@ -355,6 +355,177 @@ resonite_translations = {
'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'
}
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')
]
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')
]
}
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",
@@ -422,4 +593,5 @@ rigify_unnecessary_bones = [
'lid', 'lid',
'heel', 'heel',
'pelvis.' 'pelvis.'
] ]
+117 -103
View File
@@ -44,6 +44,117 @@ def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
class AvatarToolkitSceneProperties(PropertyGroup): class AvatarToolkitSceneProperties(PropertyGroup):
"""Property group containing Avatar Toolkit scene-level settings and properties""" """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( avatar_toolkit_updater_version_list: EnumProperty(
items=get_version_list, items=get_version_list,
@@ -396,12 +507,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"),
@@ -413,104 +518,13 @@ class AvatarToolkitSceneProperties(PropertyGroup):
description=t('MergeArmature.cleanup_shape_keys_desc'), description=t('MergeArmature.cleanup_shape_keys_desc'),
default=True default=True
) )
material_search_filter: StringProperty( merge_twist_bones: BoolProperty(
name=t("TextureAtlas.search_materials"), name=t("Tools.merge_twist_bones"),
description=t("TextureAtlas.search_materials_desc"), description=t("Tools.merge_twist_bones_desc"),
default="" default=True
) )
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
)
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 -1
View File
@@ -4,11 +4,12 @@ import bpy_extras
from numpy import double from numpy import double
from typing import Set, Dict 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 bpy.types import Context, Operator
from ..core.translations import t from ..core.translations import t
from ..core.dictionaries import bone_names, resonite_translations from ..core.dictionaries import bone_names, resonite_translations
from ..core.logging_setup import logger from ..core.logging_setup import logger
from ..core.armature_validation import validate_armature
import re import re
from .resonite_loader import resonite_animx, resonite_types from .resonite_loader import resonite_animx, resonite_types
+3 -3
View File
@@ -7,12 +7,12 @@ from ...core.logging_setup import logger
from ...core.translations import t from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
validate_armature,
get_all_meshes, get_all_meshes,
ProgressTracker, ProgressTracker,
calculate_bone_orientation, calculate_bone_orientation,
add_armature_modifier add_armature_modifier
) )
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_AttachMesh(Operator): class AvatarToolkit_OT_AttachMesh(Operator):
"""Operator to attach a mesh to an armature bone with automatic weight setup""" """Operator to attach a mesh to an armature bone with automatic weight setup"""
@@ -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:
+1 -1
View File
@@ -18,11 +18,11 @@ from ..core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
get_armature_list, get_armature_list,
validate_armature,
validate_mesh_for_pose, validate_mesh_for_pose,
cache_vertex_positions, cache_vertex_positions,
apply_vertex_positions apply_vertex_positions
) )
from ..core.armature_validation import validate_armature
VALID_EYE_NAMES: Dict[str, List[str]] = { VALID_EYE_NAMES: Dict[str, List[str]] = {
'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'], 'left': ['LeftEye', 'Eye_L', 'eye_L', 'eye.L', 'EyeLeft', 'left_eye', 'l_eye'],
+1 -1
View File
@@ -6,13 +6,13 @@ from ..core.logging_setup import logger
from ..core.common import ( from ..core.common import (
ProgressTracker, ProgressTracker,
get_active_armature, get_active_armature,
validate_armature,
get_vertex_weights, get_vertex_weights,
transfer_vertex_weights, transfer_vertex_weights,
get_all_meshes get_all_meshes
) )
from ..core.translations import t from ..core.translations import t
from ..core.dictionaries import bone_names, dont_delete_these_main_bones 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): class AVATAR_TOOLKIT_OT_StandardizeMmd(Operator):
"""MMD Bone standardization system""" """MMD Bone standardization system"""
+2 -2
View File
@@ -14,10 +14,10 @@ from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature,
clear_unused_data_blocks, clear_unused_data_blocks,
ProgressTracker ProgressTracker
) )
from ...core.armature_validation import validate_armature
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool: def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
"""Compare two texture nodes for matching properties and image data""" """Compare two texture nodes for matching properties and image data"""
@@ -92,7 +92,7 @@ class AvatarToolkit_OT_CombineMaterials(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 return valid
def execute(self, context: Context) -> Set[str]: def execute(self, context: Context) -> Set[str]:
+3 -3
View File
@@ -6,11 +6,11 @@ from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature,
validate_meshes, validate_meshes,
join_mesh_objects, join_mesh_objects,
ProgressTracker ProgressTracker
) )
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_JoinAllMeshes(Operator): class AvatarToolkit_OT_JoinAllMeshes(Operator):
"""Operator to join all meshes in the scene""" """Operator to join all meshes in the scene"""
@@ -25,7 +25,7 @@ class AvatarToolkit_OT_JoinAllMeshes(Operator):
if not armature: if not armature:
return False return False
valid: bool valid: bool
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]:
@@ -69,7 +69,7 @@ class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
if not armature: if not armature:
return False return False
valid: bool valid: bool
valid, _ = validate_armature(armature) valid, _, _ = validate_armature(armature)
return (valid and return (valid and
context.mode == 'OBJECT' and context.mode == 'OBJECT' and
len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1) len([obj for obj in context.selected_objects if obj.type == 'MESH']) > 1)
+3 -3
View File
@@ -7,8 +7,8 @@ from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature
) )
from ...core.armature_validation import validate_armature
# Constants # Constants
MERGE_ITERATION_COUNT = 20 MERGE_ITERATION_COUNT = 20
@@ -88,7 +88,7 @@ class AvatarToolkit_OT_RemoveDoublesAdvanced(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 return valid
def execute(self, context: Context) -> set[str]: def execute(self, context: Context) -> set[str]:
@@ -111,7 +111,7 @@ class AvatarToolkit_OT_RemoveDoubles(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 return valid
def draw(self, context: Context) -> None: def draw(self, context: Context) -> None:
+3 -3
View File
@@ -8,13 +8,13 @@ from ..core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
apply_pose_as_rest, apply_pose_as_rest,
validate_armature,
cache_vertex_positions, cache_vertex_positions,
apply_vertex_positions, apply_vertex_positions,
validate_mesh_for_pose, validate_mesh_for_pose,
process_armature_modifiers, process_armature_modifiers,
ProgressTracker ProgressTracker
) )
from ..core.armature_validation import validate_armature
class BatchPoseOperationMixin: class BatchPoseOperationMixin:
"""Base class for batch pose operations""" """Base class for batch pose operations"""
@@ -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]:
+6 -5
View File
@@ -4,7 +4,8 @@ from bpy.types import Operator, Context
from typing import Set from typing import Set
from ...core.translations import t from ...core.translations import t
from ...core.logging_setup import logger 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): class AvatarToolkit_OT_ApplyTransforms(Operator):
"""Apply all transformations to armature and associated meshes""" """Apply all transformations to armature and associated meshes"""
@@ -18,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:
@@ -66,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:
+5 -6
View File
@@ -5,12 +5,11 @@ from typing import Optional, Dict, Any, List, Tuple
from ...core.translations import t from ...core.translations import t
from ...core.common import ( from ...core.common import (
get_active_armature, get_active_armature,
validate_armature,
get_all_meshes, get_all_meshes,
ProgressTracker, ProgressTracker,
validate_bone_hierarchy,
restore_bone_transforms restore_bone_transforms
) )
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
def duplicate_bone(bone: EditBone) -> EditBone: def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone""" """Create a duplicate of the given bone"""
@@ -35,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)
@@ -129,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"""
+4 -3
View File
@@ -4,8 +4,9 @@ from typing import Set, Dict, Optional
from bpy.types import Operator, Context from bpy.types import Operator, Context
from ...core.translations import t from ...core.translations import t
from ...core.logging_setup import logger 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.dictionaries import bone_names, resonite_translations
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_ConvertResonite(Operator): class AvatarToolkit_OT_ConvertResonite(Operator):
"""Convert armature bone names to Resonite format with progress tracking and validation""" """Convert armature bone names to Resonite format with progress tracking and validation"""
@@ -19,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)
+4 -3
View File
@@ -4,7 +4,8 @@ from typing import Set, List
from bpy.types import Operator, Context, Armature, EditBone from bpy.types import Operator, Context, Armature, EditBone
from ...core.translations import t from ...core.translations import t
from ...core.logging_setup import logger 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): class AvatarToolkit_OT_ConnectBones(Operator):
"""Connect disconnected bones in chain""" """Connect disconnected bones in chain"""
@@ -18,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:
+6 -5
View File
@@ -1,7 +1,8 @@
import bpy import bpy
from bpy.types import Operator, Context from bpy.types import Operator, Context
from ...core.translations import t 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): class AvatarToolKit_OT_SeparateByMaterials(Operator):
"""Operator to separate mesh by materials""" """Operator to separate mesh by materials"""
@@ -16,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"""
@@ -48,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"""
+3 -4
View File
@@ -9,10 +9,10 @@ from ..core.logging_setup import logger
from ..core.translations import t from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
validate_armature,
get_all_meshes, get_all_meshes,
validate_mesh_for_pose validate_mesh_for_pose
) )
from ..core.armature_validation import validate_armature
class VisemeCache: class VisemeCache:
"""Manages caching of generated viseme shape data for performance optimization""" """Manages caching of generated viseme shape data for performance optimization"""
@@ -138,9 +138,8 @@ 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
@@ -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'
+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",
+1 -1
View File
@@ -6,9 +6,9 @@ from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
validate_armature,
get_armature_list get_armature_list
) )
from ..core.armature_validation import validate_armature
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator): class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
"""Search operator for selecting target armature to merge into""" """Search operator for selecting target armature to merge into"""
+75 -15
View File
@@ -14,7 +14,6 @@ from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
clear_default_objects, clear_default_objects,
validate_armature,
get_armature_list, get_armature_list,
get_armature_stats get_armature_stats
) )
@@ -24,6 +23,7 @@ from ..functions.pose_mode import (
AvatarToolkit_OT_ApplyPoseAsShapekey, AvatarToolkit_OT_ApplyPoseAsShapekey,
AvatarToolkit_OT_ApplyPoseAsRest AvatarToolkit_OT_ApplyPoseAsRest
) )
from ..core.armature_validation import validate_armature
class AvatarToolKit_OT_ExportFBX(Operator): class AvatarToolKit_OT_ExportFBX(Operator):
"""Export selected objects as FBX""" """Export selected objects as FBX"""
@@ -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,28 +84,87 @@ 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 is_valid, messages, is_acceptable = validate_armature(active_armature)
messages: List[str]
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 not is_valid:
row: UILayout = info_box.row() # Display non-standard bones and hierarchy issues
split: UILayout = row.split(factor=0.6) 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])
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') 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: elif is_valid and is_acceptable:
# Display validation failure messages # Show acceptable standard message
for message in messages: info_box.label(text=messages[0], icon='INFO')
info_box.label(text=message, icon='ERROR') 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')
# 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()