diff --git a/core/armature_validation.py b/core/armature_validation.py index cd74c5b..3bc193b 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -16,6 +16,8 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: """ 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 @@ -24,68 +26,76 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: return False, [t("Armature.validation.basic_check_failed")], False 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 bone_list = "\n".join([f"- {bone}" for bone in found_bones.keys()]) messages.append(t("Armature.validation.found_bones", bones=bone_list)) - # Check each bone against our standard - non_standard_bones = [] - required_patterns = [ - 'Hips', 'Spine', 'Chest', 'Neck', 'Head', - 'Upper', 'Lower', 'Hand', 'Foot', 'Toe', - 'Thumb', 'Index', 'Middle', 'Ring', 'Pinky', - 'Eye' - ] - - for bone_name in found_bones: - if any(pattern in bone_name for pattern in required_patterns): - if bone_name not in standard_bones.values(): - non_standard_bones.append(bone_name) - - if non_standard_bones: - non_standard_list = "\n".join([f"- {bone}" for bone in non_standard_bones]) - messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list)) - + # 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]) - messages.append(t("Armature.validation.missing_bones", bones=missing_list)) - + 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): - messages.append(t("Armature.validation.invalid_hierarchy", + hierarchy_messages.append(t("Armature.validation.invalid_hierarchy", parent=parent, child=child)) # Validate symmetry - symmetry_pairs = [('arm', 'l', 'r'), ('leg', 'l', 'r')] + symmetry_pairs = [('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)) + 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')): - messages.append(t("Armature.validation.asymmetric_hand_wrist")) - + 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): - messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) + 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)) - is_valid = len(messages) == 0 + # 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"), @@ -103,22 +113,31 @@ def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_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""" - left_patterns: List[str] = [ - f"{base}.{left}", - f"{base}_{left}", - f"{left}_{base}" - ] + # Extract left and right bone names from both hierarchies + left_bone_names = set() + right_bone_names = set() - right_patterns: List[str] = [ - f"{base}.{right}", - f"{base}_{right}", - f"{right}_{base}" - ] + # 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) - left_exists: bool = any(pattern in bones for pattern in left_patterns) - right_exists: bool = any(pattern in bones for pattern in right_patterns) + # 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 and right_exists + 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""" diff --git a/core/properties.py b/core/properties.py index 6b5f28d..fa1b0b5 100644 --- a/core/properties.py +++ b/core/properties.py @@ -44,6 +44,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, @@ -407,121 +518,12 @@ class AvatarToolkitSceneProperties(PropertyGroup): description=t('MergeArmature.cleanup_shape_keys_desc'), default=True ) - - 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 - ) merge_twist_bones: BoolProperty( name=t("Tools.merge_twist_bones"), description=t("Tools.merge_twist_bones_desc"), default=True ) - - 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 - ) def register() -> None: """Register the Avatar Toolkit property group""" diff --git a/functions/optimization/materials_tools.py b/functions/optimization/materials_tools.py index 95f54f8..b6983d4 100644 --- a/functions/optimization/materials_tools.py +++ b/functions/optimization/materials_tools.py @@ -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]: diff --git a/functions/optimization/mesh_tools.py b/functions/optimization/mesh_tools.py index 19a23c4..825b493 100644 --- a/functions/optimization/mesh_tools.py +++ b/functions/optimization/mesh_tools.py @@ -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) diff --git a/functions/optimization/remove_doubles.py b/functions/optimization/remove_doubles.py index ef8c608..e5c20e5 100644 --- a/functions/optimization/remove_doubles.py +++ b/functions/optimization/remove_doubles.py @@ -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: diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index d20db3a..0d749e6 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -88,75 +88,81 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): info_box = col.box() - if is_valid: - if is_acceptable: - # Show acceptable standard message + 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]) - - # 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() - split = row.split(factor=0.6) - split.label(text=t("QuickAccess.valid_armature"), icon='CHECKMARK') - 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: - # 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) + 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 = get_armature_stats(active_armature) + split.label(text=t("QuickAccess.bones_count", count=stats['bone_count'])) - # Main validation status - validation_box = info_box.box() - row = validation_box.row() - row.alert = True - row.label(text=t("Validation.status.failed")) + if stats['has_pose']: + info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') + 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]) - # 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) + # 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 @@ -196,4 +202,3 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): button_row.scale_y = 1.5 button_row.operator("avatar_toolkit.import", text=t("QuickAccess.import"), icon='IMPORT') button_row.operator("avatar_toolkit.export", text=t("QuickAccess.export"), icon='EXPORT') -