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
#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.
#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]] = {}
bool_material_list_expand: dict[str,bool] = {}
@@ -46,7 +45,6 @@ class MaterialListBool:
if mat_slot.material:
if mat_slot.material not in newlist:
newlist.append(mat_slot.material)
still_the_same: bool = True
if bpy.context.scene.name in MaterialListBool.old_list:
for item in newlist:
@@ -60,7 +58,6 @@ class MaterialListBool:
else:
still_the_same = False
MaterialListBool.bool_material_list_expand[bpy.context.scene.name] = still_the_same
return MaterialListBool.bool_material_list_expand[bpy.context.scene.name]
class ProgressTracker:
@@ -111,89 +108,6 @@ def get_armature_list(self: Optional[Any] = None, context: Optional[Context] = N
return [('NONE', t("Armature.validation.no_armature"), '')]
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:
"""Automatically select armature if only one exists in scene"""
armatures: List[Tuple[str, str, str]] = get_armature_list(context)
+172
View File
@@ -355,6 +355,177 @@ resonite_translations = {
'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 = {
"DEF-spine": "Hips",
"DEF-spine.001": "Spine",
@@ -423,3 +594,4 @@ rigify_unnecessary_bones = [
'heel',
'pelvis.'
]
+115 -101
View File
@@ -45,6 +45,117 @@ def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
class AvatarToolkitSceneProperties(PropertyGroup):
"""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(
items=get_version_list,
name=t("Scene.avatar_toolkit_updater_version_list.name"),
@@ -396,12 +507,6 @@ class AvatarToolkitSceneProperties(PropertyGroup):
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(
name=t("Tools.list_only_mode"),
description=t("Tools.list_only_mode_desc"),
@@ -414,103 +519,12 @@ class AvatarToolkitSceneProperties(PropertyGroup):
default=True
)
material_search_filter: StringProperty(
name=t("TextureAtlas.search_materials"),
description=t("TextureAtlas.search_materials_desc"),
default=""
merge_twist_bones: BoolProperty(
name=t("Tools.merge_twist_bones"),
description=t("Tools.merge_twist_bones_desc"),
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:
"""Register the Avatar Toolkit property group"""
logger.info("Registering Avatar Toolkit properties")
+2 -1
View File
@@ -4,11 +4,12 @@ import bpy_extras
from numpy import double
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 ..core.translations import t
from ..core.dictionaries import bone_names, resonite_translations
from ..core.logging_setup import logger
from ..core.armature_validation import validate_armature
import re
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.common import (
get_active_armature,
validate_armature,
get_all_meshes,
ProgressTracker,
calculate_bone_orientation,
add_armature_modifier
)
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_AttachMesh(Operator):
"""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)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
try:
+1 -1
View File
@@ -18,11 +18,11 @@ from ..core.common import (
get_active_armature,
get_all_meshes,
get_armature_list,
validate_armature,
validate_mesh_for_pose,
cache_vertex_positions,
apply_vertex_positions
)
from ..core.armature_validation import validate_armature
VALID_EYE_NAMES: Dict[str, List[str]] = {
'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 (
ProgressTracker,
get_active_armature,
validate_armature,
get_vertex_weights,
transfer_vertex_weights,
get_all_meshes
)
from ..core.translations import t
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):
"""MMD Bone standardization system"""
+2 -2
View File
@@ -14,10 +14,10 @@ from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
clear_unused_data_blocks,
ProgressTracker
)
from ...core.armature_validation import validate_armature
def textures_match(tex1: ShaderNodeTexImage, tex2: ShaderNodeTexImage) -> bool:
"""Compare two texture nodes for matching properties and image data"""
@@ -92,7 +92,7 @@ class AvatarToolkit_OT_CombineMaterials(Operator):
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
+3 -3
View File
@@ -6,11 +6,11 @@ from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
validate_meshes,
join_mesh_objects,
ProgressTracker
)
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_JoinAllMeshes(Operator):
"""Operator to join all meshes in the scene"""
@@ -25,7 +25,7 @@ class AvatarToolkit_OT_JoinAllMeshes(Operator):
if not armature:
return False
valid: bool
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
@@ -69,7 +69,7 @@ class AvatarToolkit_OT_JoinSelectedMeshes(Operator):
if not armature:
return False
valid: bool
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return (valid and
context.mode == 'OBJECT' and
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 (
get_active_armature,
get_all_meshes,
validate_armature
)
from ...core.armature_validation import validate_armature
# Constants
MERGE_ITERATION_COUNT = 20
@@ -88,7 +88,7 @@ class AvatarToolkit_OT_RemoveDoublesAdvanced(Operator):
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> set[str]:
@@ -111,7 +111,7 @@ class AvatarToolkit_OT_RemoveDoubles(Operator):
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
def draw(self, context: Context) -> None:
+3 -3
View File
@@ -8,13 +8,13 @@ from ..core.common import (
get_active_armature,
get_all_meshes,
apply_pose_as_rest,
validate_armature,
cache_vertex_positions,
apply_vertex_positions,
validate_mesh_for_pose,
process_armature_modifiers,
ProgressTracker
)
from ..core.armature_validation import validate_armature
class BatchPoseOperationMixin:
"""Base class for batch pose operations"""
@@ -23,7 +23,7 @@ class BatchPoseOperationMixin:
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid and context.mode == 'POSE'
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)
if not armature or context.mode == "POSE":
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid
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 ...core.translations import t
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):
"""Apply all transformations to armature and associated meshes"""
@@ -18,8 +19,8 @@ class AvatarToolkit_OT_ApplyTransforms(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT'
valid, _, _ = validate_armature(armature)
return valid and context.mode == 'OBJECT'
def execute(self, context: Context) -> Set[str]:
try:
@@ -66,8 +67,8 @@ class AvatarToolkit_OT_CleanShapekeys(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
valid, _, _ = validate_armature(armature)
return valid and context.mode == 'OBJECT' and len(get_all_meshes(context)) > 0
def execute(self, context: Context) -> Set[str]:
try:
+5 -6
View File
@@ -5,12 +5,11 @@ from typing import Optional, Dict, Any, List, Tuple
from ...core.translations import t
from ...core.common import (
get_active_armature,
validate_armature,
get_all_meshes,
ProgressTracker,
validate_bone_hierarchy,
restore_bone_transforms
)
from ...core.armature_validation import validate_armature, validate_bone_hierarchy
def duplicate_bone(bone: EditBone) -> EditBone:
"""Create a duplicate of the given bone"""
@@ -35,8 +34,8 @@ class AvatarToolKit_OT_CreateDigitigradeLegs(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return (is_valid and
valid, _, _ = validate_armature(armature)
return (valid and
context.mode == 'EDIT_ARMATURE' and
context.selected_editable_bones is not None and
len(context.selected_editable_bones) == 2)
@@ -129,8 +128,8 @@ class AvatarToolKit_OT_DeleteBoneConstraints(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> set[str]:
"""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 ...core.translations import t
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.armature_validation import validate_armature
class AvatarToolkit_OT_ConvertResonite(Operator):
"""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)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
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 ...core.translations import t
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):
"""Connect disconnected bones in chain"""
@@ -18,8 +19,8 @@ class AvatarToolkit_OT_ConnectBones(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
return is_valid
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
try:
+6 -5
View File
@@ -1,7 +1,8 @@
import bpy
from bpy.types import Operator, Context
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):
"""Operator to separate mesh by materials"""
@@ -16,10 +17,10 @@ class AvatarToolKit_OT_SeparateByMaterials(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return (context.active_object and
context.active_object.type == 'MESH' and
is_valid)
valid)
def execute(self, context: Context) -> set[str]:
"""Execute the separation operation"""
@@ -48,10 +49,10 @@ class AvatarToolKit_OT_SeparateByLooseParts(Operator):
armature = get_active_armature(context)
if not armature:
return False
is_valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return (context.active_object and
context.active_object.type == 'MESH' and
is_valid)
valid)
def execute(self, context: Context) -> set[str]:
"""Execute the separation operation"""
+2 -1
View File
@@ -1,10 +1,11 @@
import bpy
from typing import Dict, List, Set, Optional, Tuple, Any
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.translations import t
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):
"""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.common import (
get_active_armature,
validate_armature,
get_all_meshes,
validate_mesh_for_pose
)
from ..core.armature_validation import validate_armature
class VisemeCache:
"""Manages caching of generated viseme shape data for performance optimization"""
@@ -138,10 +138,9 @@ class ATOOLKIT_OT_preview_visemes(Operator):
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
return valid and mesh_obj and mesh_obj.type == 'MESH'
def execute(self, context: Context) -> Set[str]:
props = context.scene.avatar_toolkit
mesh = context.active_object
@@ -197,7 +196,7 @@ class ATOOLKIT_OT_create_visemes(Operator):
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
valid, _, _ = validate_armature(armature)
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.asymmetric_bones": "Missing symmetric bones for {bone}",
"Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists",
"Armature.validation.found_bones": "Found bones in armature:\n- {bones}",
"Armature.validation.non_standard_bones": "Non-standard bones found:\n- {bones}",
"Validation.section.found_bones": "Found Bones",
"Validation.section.non_standard": "Non-Standard Bones",
"Validation.section.hierarchy": "Hierarchy Issues",
"Validation.status.failed": "Validation has failed",
"Validation.message.failed.line1": "Armature validation has failed",
"Validation.message.failed.line2": "Please check below what the",
"Validation.message.failed.line3": "issues are",
"Mesh.validation.no_data": "No mesh data",
"Mesh.validation.no_vertex_groups": "No vertex groups found",
+1 -1
View File
@@ -6,9 +6,9 @@ from ..core.translations import t
from ..core.common import (
get_active_armature,
get_all_meshes,
validate_armature,
get_armature_list
)
from ..core.armature_validation import validate_armature
class AvatarToolkit_OT_SearchMergeArmatureInto(Operator):
"""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 (
get_active_armature,
clear_default_objects,
validate_armature,
get_armature_list,
get_armature_stats
)
@@ -24,6 +23,7 @@ from ..functions.pose_mode import (
AvatarToolkit_OT_ApplyPoseAsShapekey,
AvatarToolkit_OT_ApplyPoseAsRest
)
from ..core.armature_validation import validate_armature
class AvatarToolKit_OT_ExportFBX(Operator):
"""Export selected objects as FBX"""
@@ -70,6 +70,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
def draw(self, context: Context) -> None:
"""Draw the panel layout"""
layout: UILayout = self.layout
props = context.scene.avatar_toolkit
# Armature Selection Box
armature_box: UILayout = layout.box()
@@ -83,28 +84,87 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
# Armature Validation
active_armature: Optional[Object] = get_active_armature(context)
if active_armature:
is_valid: bool
messages: List[str]
is_valid, messages = validate_armature(active_armature)
is_valid, messages, is_acceptable = validate_armature(active_armature)
# Create info box for all validation information
info_box: UILayout = col.box()
info_box = col.box()
if is_valid:
row: UILayout = info_box.row()
split: UILayout = row.split(factor=0.6)
if not is_valid:
# Display non-standard bones and hierarchy issues
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')
stats: Dict[str, int] = get_armature_stats(active_armature)
stats = get_armature_stats(active_armature)
split.label(text=t("QuickAccess.bones_count", count=stats['bone_count']))
if stats['has_pose']:
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
else:
# Display validation failure messages
for message in messages:
info_box.label(text=message, icon='ERROR')
elif is_valid and 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])
# Validation Mode Warnings - always show in info box
# 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
validation_mode = context.scene.avatar_toolkit.validation_mode
if validation_mode == 'BASIC':
warning_row = info_box.box()