diff --git a/__init__.py b/__init__.py index 59a9275..14dabc2 100644 --- a/__init__.py +++ b/__init__.py @@ -18,10 +18,25 @@ def register(): from .core import auto_load print("Starting registration") + + # Make sure to initialize logging first + from .core.logging_setup import configure_logging + configure_logging(False) + + # Then initialize the addon auto_load.init() + + # Register classes in proper order auto_load.register() + + # Verify property registration + import bpy + if not hasattr(bpy.types.Scene, "avatar_toolkit"): + from .core.properties import register as register_properties + register_properties() + print("Registration complete") def unregister(): from .core import auto_load - auto_load.unregister() + auto_load.unregister() \ No newline at end of file diff --git a/core/addon_preferences.py b/core/addon_preferences.py index b9fdd63..1bea83a 100644 --- a/core/addon_preferences.py +++ b/core/addon_preferences.py @@ -59,4 +59,5 @@ def get_addon_preferences(context): if not os.path.exists(PREFERENCES_FILE): save_preference("language", 0) # Set default language to 0 (auto) save_preference("validation_mode", "STRICT") # Set default validation mode - save_preference("enable_logging", False) # Set default logging mode \ No newline at end of file + save_preference("enable_logging", False) # Set default logging mode + save_preference("highlight_problem_bones", True) # Set default bone highlighting \ No newline at end of file diff --git a/core/armature_validation.py b/core/armature_validation.py index 3bc193b..ef8ad2a 100644 --- a/core/armature_validation.py +++ b/core/armature_validation.py @@ -1,6 +1,9 @@ import bpy -from typing import Tuple, List, Dict, Set, Optional -from bpy.types import Object, Bone +import math +from mathutils import Vector, Color +from typing import Tuple, List, Dict, Set, Optional, Union +from bpy.types import Object, Bone, Operator +from ..core.common import get_armature_list, get_active_armature from ..core.translations import t from ..core.dictionaries import ( standard_bones, @@ -9,23 +12,35 @@ from ..core.dictionaries import ( acceptable_bone_hierarchy, acceptable_bone_names ) +from ..core.logging_setup import logger -def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: +def validate_armature(armature: Object, detailed_messages: bool = False) -> Union[Tuple[bool, List[str], bool], Tuple[bool, List[str], bool, List[str], List[str], List[str]]]: """ - Validates armature and returns (is_valid, messages, is_acceptable_standard) + Validates armature and returns validation results """ + logger.debug(f"Validating armature: {armature.name if armature else 'None'}") validation_mode = bpy.context.scene.avatar_toolkit.validation_mode messages: List[str] = [] hierarchy_messages: List[str] = [] non_standard_messages: List[str] = [] + scale_messages: List[str] = [] if validation_mode == 'NONE': - return True, [], False + logger.debug("Validation mode is NONE, skipping validation") + if detailed_messages: + return True, [], False, [], [], [] + else: + return True, [], False if not armature or armature.type != 'ARMATURE' or not armature.data.bones: - return False, [t("Armature.validation.basic_check_failed")], False + logger.warning("Basic armature check failed") + if detailed_messages: + return False, [t("Armature.validation.basic_check_failed")], False, [], [], [] + else: + return False, [t("Armature.validation.basic_check_failed")], False found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones} + logger.debug(f"Found {len(found_bones)} bones in armature") is_acceptable = check_acceptable_standards(found_bones) # List all bones in armature @@ -39,31 +54,46 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: if missing_bones: missing_list = "\n".join([f"- {bone}" for bone in missing_bones]) + logger.warning(f"Missing essential bones: {', '.join(missing_bones)}") hierarchy_messages.append(t("Armature.validation.missing_bones", bones=missing_list)) if validation_mode == 'STRICT': + logger.debug("Performing strict validation") + # Add scale issue detection in STRICT mode + scale_issues = detect_scale_issues(found_bones) + if scale_issues: + logger.warning(f"Found {len(scale_issues)} scale issues") + # CHANGE: Don't combine into a single string, keep as separate items + scale_messages.extend(scale_issues) + # 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): + logger.warning(f"Invalid hierarchy: {parent} -> {child}") hierarchy_messages.append(t("Armature.validation.invalid_hierarchy", parent=parent, child=child)) # Validate symmetry + logger.debug("Validating bone 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): + logger.warning(f"Asymmetric bones found: {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')): + logger.warning("Asymmetric hand/wrist bones found") hierarchy_messages.append(t("Armature.validation.asymmetric_hand_wrist")) - + # Validate finger hierarchies + logger.debug("Validating 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): + logger.warning(f"Invalid finger hierarchy: {finger_chain[0]}") hierarchy_messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0])) # Non-standard bones check @@ -83,27 +113,39 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]: non_standard_bones.append(bone_name) if non_standard_bones: + logger.warning(f"Found {len(non_standard_bones)} 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 + is_valid = len(non_standard_messages) == 0 and len(hierarchy_messages) == 0 and len(scale_messages) == 0 if not is_valid and is_acceptable: if non_standard_bones: - return False, messages, False + logger.info("Armature has non-standard bones but is acceptable") + if detailed_messages: + return False, messages, False, hierarchy_messages, scale_messages, non_standard_messages + else: + return False, messages, False + logger.info("Armature meets acceptable standards") 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 + if detailed_messages: + return True, messages, True, [], [], [] + else: + return True, messages, True + + logger.info(f"Armature validation complete. Valid: {is_valid}") + if detailed_messages: + return is_valid, messages, False, hierarchy_messages, scale_messages, non_standard_messages + else: + 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""" @@ -148,6 +190,7 @@ def validate_finger_chain(bones: Dict[str, Bone], chain: Tuple[str, ...]) -> boo def check_acceptable_standards(bones: Dict[str, Bone]) -> bool: """Check if armature matches acceptable non-standard hierarchy""" + logger.debug("Checking for acceptable standards") # Check if bones exist in acceptable list for bone_category, acceptable_names in acceptable_bone_names.items(): found = False @@ -156,12 +199,360 @@ def check_acceptable_standards(bones: Dict[str, Bone]) -> bool: found = True break if not found: + logger.debug(f"Missing acceptable bone for category: {bone_category}") 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): + logger.debug(f"Invalid acceptable hierarchy: {parent} -> {child}") return False + logger.debug("Armature meets acceptable standards") return True + +def validate_tpose(armature): + """Validate if armature is in a proper T-pose""" + logger.debug(f"Validating T-pose for armature: {armature.name if armature else 'None'}") + if not armature or armature.type != 'ARMATURE': + logger.warning("No valid armature for T-pose validation") + return False, [t("Validation.tpose.no_armature")] + + issues = [] + + if armature.mode == 'POSE': + bones_collection = armature.pose.bones + get_direction = lambda bone: bone.matrix.to_3x3().col[1].normalized() + else: + bones_collection = armature.data.bones + get_direction = lambda bone: bone.y_axis + + # Get left and right upper arm bones using standard bone names + left_arm = None + right_arm = None + + left_arm_candidates = [standard_bones['left_arm']] # UpperArm.L + if 'arm_l' in acceptable_bone_names: + left_arm_candidates.extend(acceptable_bone_names['arm_l']) + + right_arm_candidates = [standard_bones['right_arm']] # UpperArm.R + if 'arm_r' in acceptable_bone_names: + right_arm_candidates.extend(acceptable_bone_names['arm_r']) + + for name in left_arm_candidates: + if name in armature.data.bones: + left_arm = armature.data.bones[name] + logger.debug(f"Found left arm bone: {name}") + break + + for name in right_arm_candidates: + if name in armature.data.bones: + right_arm = armature.data.bones[name] + logger.debug(f"Found right arm bone: {name}") + break + + # Check arm bones are horizontal + if left_arm: + direction = left_arm.y_axis + if abs(direction.x) < 0.7: # Not pointing mostly along X axis + logger.warning("Left arm is not horizontal") + issues.append(t("Validation.tpose.left_arm_not_horizontal")) + + if right_arm: + direction = right_arm.y_axis + if abs(direction.x) < 0.7: # Not pointing mostly along X axis + logger.warning("Right arm is not horizontal") + issues.append(t("Validation.tpose.right_arm_not_horizontal")) + + spine = None + spine_candidates = [standard_bones['spine']] # Spine + if 'spine' in acceptable_bone_names: + spine_candidates.extend(acceptable_bone_names['spine']) + + for name in spine_candidates: + if name in armature.data.bones: + spine = armature.data.bones[name] + logger.debug(f"Found spine bone: {name}") + break + + if spine: + direction = spine.y_axis + if abs(direction.z) < 0.7: # Not pointing mostly along Z axis + logger.warning("Spine is not vertical") + issues.append(t("Validation.tpose.spine_not_vertical")) + + if issues: + logger.warning(f"T-pose validation failed with {len(issues)} issues") + return False, issues + + logger.info("T-pose validation successful") + return True, [] + +def detect_scale_issues(bones): + """Detect bones with abnormal scale (too small or too large)""" + logger.debug("Detecting scale issues") + scale_issues = [] + + # Calculate median bone length for reference (more robust than average) + lengths = [bone.length for bone in bones.values()] + lengths.sort() + + if not lengths: + logger.debug("No bones with length found") + return [] + + median_length = lengths[len(lengths) // 2] + + # Filter out zero-length bones for standard deviation calculation + non_zero_lengths = [l for l in lengths if l > 0.0001] + + if not non_zero_lengths: + logger.debug("No non-zero length bones found") + return [] + + mean = sum(non_zero_lengths) / len(non_zero_lengths) + variance = sum((l - mean) ** 2 for l in non_zero_lengths) / len(non_zero_lengths) + std_dev = math.sqrt(variance) + + small_threshold = max(median_length * 0.05, mean - 3 * std_dev) + large_threshold = min(median_length * 15, mean + 5 * std_dev) + + logger.debug(f"Scale thresholds - small: {small_threshold}, large: {large_threshold}") + + # Get finger bones from standard and acceptable bone dictionaries + finger_bone_names = set() + + for key in standard_bones: + if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']): + finger_bone_names.add(standard_bones[key]) + + for key, names in acceptable_bone_names.items(): + if any(finger in key.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger']): + finger_bone_names.update(names) + + for name, bone in bones.items(): + is_finger = (name in finger_bone_names or + any(finger in name.lower() for finger in ['thumb', 'index', 'middle', 'ring', 'pinky', 'finger'])) + + if bone.length < small_threshold and not is_finger: + logger.debug(f"Bone {name} is too small: {bone.length}") + scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_small')} ({bone.length:.4f})") + elif bone.length > large_threshold: + logger.debug(f"Bone {name} is too large: {bone.length}") + scale_issues.append(f"- {name}: {t('Validation.scale_issue.too_large')} ({bone.length:.4f})") + + logger.debug(f"Found {len(scale_issues)} scale issues") + return scale_issues + +def clear_bone_highlighting(armature: Object) -> None: + """Clear bone highlighting by removing bone collections and resetting colors""" + logger.debug(f"Clearing bone highlighting for armature: {armature.name if armature else 'None'}") + if not armature or armature.type != 'ARMATURE': + logger.warning("No valid armature for clearing bone highlighting") + return + + current_mode = bpy.context.mode + + collection_name = "Problem Bones" + if collection_name in armature.data.collections: + problem_collection = armature.data.collections[collection_name] + armature.data.collections.remove(problem_collection) + logger.debug("Removed problem bones collection") + + for bone in armature.data.bones: + bone.color.palette = 'DEFAULT' + + if len(armature.data.collections) == 0: + armature.data.show_bone_colors = False + logger.debug("Disabled bone colors display") + + logger.info("Bone highlighting cleared") + return + +class AvatarToolkit_OT_HighlightProblemBones(Operator): + """Highlight bones that fail validation in the 3D viewport""" + bl_idname = "avatar_toolkit.highlight_problem_bones" + bl_label = t("Validation.highlight_problem_bones") + bl_description = t("Validation.highlight_problem_bones_desc") + + @classmethod + def poll(cls, context): + return get_active_armature(context) is not None + + def execute(self, context): + armature = get_active_armature(context) + if not armature: + logger.warning("No active armature found for highlighting problem bones") + self.report({'ERROR'}, t("Validation.no_armature")) + return {'CANCELLED'} + + logger.info(f"Highlighting problem bones for armature: {armature.name}") + + current_mode = context.mode + + if current_mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + context.view_layer.objects.active = armature + + # First remove all bone collections + collection_name = "Problem Bones" + if collection_name in armature.data.collections: + problem_collection = armature.data.collections[collection_name] + armature.data.collections.remove(problem_collection) + logger.debug("Removed existing problem bones collection") + + is_valid, messages, _ = validate_armature(armature) + + if is_valid: + logger.info("No validation issues found") + self.report({'INFO'}, t("Validation.no_issues")) + bpy.ops.object.mode_set(mode='EDIT') + return {'FINISHED'} + + problem_collection = armature.data.collections.new(name="Problem Bones") + logger.debug("Created new problem bones collection") + armature.data.show_bone_colors = True + bpy.ops.object.mode_set(mode='EDIT') + + # Extract bone names from validation messages + problem_bones = self._extract_problem_bones(messages) + + # Assign bones to collection and set colors + highlighted_count = 0 + for category, bone_names in problem_bones.items(): + for bone_name in bone_names: + if bone_name in armature.data.edit_bones: + bone = armature.data.edit_bones[bone_name] + problem_collection.assign(bone) + + if 'hierarchy' in category.lower(): + bone.color.palette = 'THEME09' # Orange + elif 'scale' in category.lower(): + bone.color.palette = 'THEME03' # Yellow + else: + bone.color.palette = 'THEME01' # Red + + highlighted_count += 1 + + logger.info(f"Highlighted {highlighted_count} problem bones") + self.report({'INFO'}, t("Validation.highlighting_complete")) + return {'FINISHED'} + + def _extract_problem_bones(self, messages): + problem_bones = { + "Hierarchy Issues": [], + "Scale Issues": [], + "Missing Bones": [] + } + + # Extract bone names from validation messages + for message in messages: + if isinstance(message, str): + # Parse message to extract bone names + for line in message.split('\n'): + if '- ' in line: + bone_name = line.split('- ')[1].strip() + if ':' in bone_name: # Handle "bone_name: message" format + bone_name = bone_name.split(':')[0].strip() + + if 'hierarchy' in message.lower(): + problem_bones["Hierarchy Issues"].append(bone_name) + elif 'scale' in message.lower(): + problem_bones["Scale Issues"].append(bone_name) + else: + problem_bones["Missing Bones"].append(bone_name) + + logger.debug(f"Extracted problem bones: {problem_bones}") + return problem_bones + +class AvatarToolkit_OT_ValidateTPose(Operator): + """Validate if armature is in a proper T-pose""" + bl_idname = "avatar_toolkit.validate_tpose" + bl_label = t("Validation.tpose.label") + bl_description = t("Validation.tpose.desc") + + @classmethod + def poll(cls, context): + return get_active_armature(context) is not None + + def execute(self, context): + armature = get_active_armature(context) + if not armature: + logger.warning("No active armature found for T-pose validation") + self.report({'ERROR'}, t("Validation.no_armature")) + return {'CANCELLED'} + + logger.info(f"Validating T-pose for armature: {armature.name}") + is_valid, messages = validate_tpose(armature) + props = context.scene.avatar_toolkit + props.tpose_validation_result = is_valid + props.tpose_validation_messages.clear() + + for msg in messages: + item = props.tpose_validation_messages.add() + item.name = msg + + props.show_tpose_validation = True + + if is_valid: + logger.info("T-pose validation successful") + self.report({'INFO'}, t("Validation.tpose.valid")) + else: + for msg in messages: + self.report({'WARNING'}, msg) + logger.warning("T-pose validation failed") + self.report({'WARNING'}, t("Validation.tpose.warning")) + + return {'FINISHED'} + +class AvatarToolkit_OT_ClearBoneHighlighting(Operator): + """Clear bone highlighting and reset bone colors""" + bl_idname = "avatar_toolkit.clear_bone_highlighting" + bl_label = t("Validation.clear_bone_highlighting") + bl_description = t("Validation.clear_bone_highlighting_desc") + + @classmethod + def poll(cls, context): + return get_active_armature(context) is not None + + def execute(self, context): + armature = get_active_armature(context) + if not armature: + logger.warning("No active armature found for clearing bone highlighting") + self.report({'ERROR'}, t("Validation.no_armature")) + return {'CANCELLED'} + + logger.info(f"Clearing bone highlighting for armature: {armature.name}") + current_mode = context.mode + + # Switch to object mode as collection editing is not possible in edit mode + if current_mode != 'OBJECT': + bpy.ops.object.mode_set(mode='OBJECT') + + context.view_layer.objects.active = armature + + collection_name = "Problem Bones" + if collection_name in armature.data.collections: + # Remove the collection + problem_collection = armature.data.collections[collection_name] + armature.data.collections.remove(problem_collection) + logger.debug("Removed problem bones collection") + + bpy.ops.object.mode_set(mode='EDIT') + + # Reset all bone colors + for bone in armature.data.edit_bones: + bone.color.palette = 'DEFAULT' + + # Turn off bone colors display if no other collections are using it + if len(armature.data.collections) == 0: + armature.data.show_bone_colors = False + logger.debug("Disabled bone colors display") + + bpy.ops.object.mode_set(mode='OBJECT') + + logger.info("Bone highlighting cleared") + self.report({'INFO'}, t("Validation.highlighting_cleared")) + return {'FINISHED'} diff --git a/core/dictionaries.py b/core/dictionaries.py index ebcbb89..36d17b2 100644 --- a/core/dictionaries.py +++ b/core/dictionaries.py @@ -498,32 +498,77 @@ acceptable_bone_hierarchy = [ ('Head', 'Eye_L'), ('Head', 'Eye_R'), ('Head', 'LeftEye'), - ('Head', 'RightEye') + ('Head', 'RightEye'), + + # Unity humanoid naming + ('Hips', 'Spine'), + ('Spine', 'Chest'), + ('Chest', 'UpperChest'), + ('UpperChest', 'Neck'), + ('Neck', 'Head'), + ('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'] + 'hips': ['Hips', 'pelvis', 'root', 'Root', 'ROOT'], + 'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'], + 'neck': ['Neck', 'neck_01', 'Neck01'], + 'head': ['Head', 'head_01', 'Head01'], + 'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft'], + 'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight'], + + 'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder'], + 'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm'], + 'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm'], + 'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand'], + 'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg'], + 'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg'], + 'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot'], + 'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase'], + + 'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder'], + 'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm'], + 'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm'], + 'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand'], + 'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg'], + 'knee_l': ['Knee.L', 'calf_l', 'CalfLeft', 'LeftShin', 'LeftLowerLeg'], + 'foot_l': ['Foot.L', 'foot_l', 'FootLeft', 'LeftFoot'], + 'toes_l': ['Toes.L', 'ball_l', 'ToeLeft', 'LeftToeBase'], + + # Add finger bones for left hand + 'thumb_0_l': ['Thumb0_L'], + 'thumb_1_l': ['Thumb1_L'], + 'thumb_2_l': ['Thumb2_L'], + 'index_1_l': ['IndexFinger1_L'], + 'index_2_l': ['IndexFinger2_L'], + 'index_3_l': ['IndexFinger3_L'], + 'middle_1_l': ['MiddleFinger1_L'], + 'middle_2_l': ['MiddleFinger2_L'], + 'middle_3_l': ['MiddleFinger3_L'], + 'ring_1_l': ['RingFinger1_L'], + 'ring_2_l': ['RingFinger2_L'], + 'ring_3_l': ['RingFinger3_L'], + + # Add finger bones for right hand + 'thumb_0_r': ['Thumb0_R', 'ThumbO_R'], + 'thumb_1_r': ['Thumb1_R'], + 'thumb_2_r': ['Thumb2_R'], + 'index_1_r': ['IndexFinger1_R'], + 'index_2_r': ['IndexFinger2_R'], + 'index_3_r': ['IndexFinger3_R'], + 'middle_1_r': ['MiddleFinger1_R'], + 'middle_2_r': ['MiddleFinger2_R'], + 'middle_3_r': ['MiddleFinger3_R'], + 'ring_1_r': ['RingFinger1_R'], + 'ring_2_r': ['RingFinger2_R'], + 'ring_3_r': ['RingFinger3_R'], + + 'breast_upper_1_l': ['BreastUpper1_L'], + 'breast_upper_2_l': ['BreastUpper2_L'], + 'breast_upper_1_r': ['BreastUpper1_R'], + 'breast_upper_2_r': ['BreastUpper2_R'] } rigify_unity_names = { diff --git a/core/logging_setup.py b/core/logging_setup.py index 1cf2137..9e86872 100644 --- a/core/logging_setup.py +++ b/core/logging_setup.py @@ -35,4 +35,11 @@ def update_logging_state(self: Any, context: Context) -> None: from .addon_preferences import save_preference enabled = self.enable_logging save_preference("enable_logging", enabled) - configure_logging(enabled) \ No newline at end of file + configure_logging(enabled) + +def highlight_problem_bones(self: Any, context: Context) -> None: + """Log when problem bones are highlighted""" + from .addon_preferences import save_preference + enabled = self.highlight_problem_bones + save_preference("highlight_problem_bones", enabled) + logger.debug(f"Problem bone highlighting {'enabled' if enabled else 'disabled'}") diff --git a/core/properties.py b/core/properties.py index 1119758..8b02f2d 100644 --- a/core/properties.py +++ b/core/properties.py @@ -18,6 +18,10 @@ from .common import get_armature_list, get_active_armature, get_all_meshes, Scen from ..functions.visemes import VisemePreview from ..functions.eye_tracking import set_rotation +class ValidationMessageItem(PropertyGroup): + """Property group for validation message items""" + name: StringProperty(name="Message") + class ZeroWeightBoneItem(PropertyGroup): """Property group for zero weight bone list items""" name: StringProperty(name="Bone Name") @@ -25,11 +29,13 @@ class ZeroWeightBoneItem(PropertyGroup): has_children: BoolProperty(name="Has Children", default=False) is_deform: BoolProperty(name="Is Deform Bone", default=False) + def update_validation_mode(self: PropertyGroup, context: Context) -> None: """Updates validation mode and saves preference""" logger.info(f"Updating validation mode to: {self.validation_mode}") save_preference("validation_mode", self.validation_mode) + def update_logging_state(self: PropertyGroup, context: Context) -> None: """Updates logging state and configures logging""" logger.info(f"Updating logging state to: {self.enable_logging}") @@ -37,11 +43,17 @@ def update_logging_state(self: PropertyGroup, context: Context) -> None: from .logging_setup import configure_logging configure_logging(self.enable_logging) + def update_shape_intensity(self: PropertyGroup, context: Context) -> None: """Updates shape key intensity and refreshes preview""" if self.viseme_preview_mode: VisemePreview.update_preview(context) +def highlight_problem_bones(self: PropertyGroup, context: Context) -> None: + """Updates problem bone highlighting state and saves preference""" + logger.info(f"Updating problem bone highlighting to: {self.highlight_problem_bones}") + save_preference("highlight_problem_bones", self.highlight_problem_bones) + def get_mesh_objects(self, context): meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH'] if not meshes: @@ -76,8 +88,8 @@ class AvatarToolkitSceneProperties(PropertyGroup): 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) + (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"), @@ -298,38 +310,37 @@ class AvatarToolkitSceneProperties(PropertyGroup): ) viseme_preview_selection: EnumProperty( - name=t("Visemes.preview_selection"), - description=t("Visemes.preview_selection_desc"), - items=[ - ('vrc.v_aa', 'AA', 'A as in "bat"'), - ('vrc.v_ch', 'CH', 'Ch as in "choose"'), - ('vrc.v_dd', 'DD', 'D as in "dog"'), - ('vrc.v_ih', 'IH', 'I as in "bit"'), - ('vrc.v_ff', 'FF', 'F as in "fox"'), - ('vrc.v_e', 'E', 'E as in "bet"'), - ('vrc.v_kk', 'KK', 'K as in "cat"'), - ('vrc.v_nn', 'NN', 'N as in "net"'), - ('vrc.v_oh', 'OH', 'O as in "hot"'), - ('vrc.v_ou', 'OU', 'O as in "go"'), - ('vrc.v_pp', 'PP', 'P as in "pat"'), - ('vrc.v_rr', 'RR', 'R as in "red"'), - ('vrc.v_sil', 'SIL', 'Silence'), - ('vrc.v_ss', 'SS', 'S as in "sit"'), - ('vrc.v_th', 'TH', 'Th as in "think"') - ], - update=lambda s, c: VisemePreview.update_preview(c) - -) + name=t("Visemes.preview_selection"), + description=t("Visemes.preview_selection_desc"), + items=[ + ('vrc.v_aa', 'AA', 'A as in "bat"'), + ('vrc.v_ch', 'CH', 'Ch as in "choose"'), + ('vrc.v_dd', 'DD', 'D as in "dog"'), + ('vrc.v_ih', 'IH', 'I as in "bit"'), + ('vrc.v_ff', 'FF', 'F as in "fox"'), + ('vrc.v_e', 'E', 'E as in "bet"'), + ('vrc.v_kk', 'KK', 'K as in "cat"'), + ('vrc.v_nn', 'NN', 'N as in "net"'), + ('vrc.v_oh', 'OH', 'O as in "hot"'), + ('vrc.v_ou', 'OU', 'O as in "go"'), + ('vrc.v_pp', 'PP', 'P as in "pat"'), + ('vrc.v_rr', 'RR', 'R as in "red"'), + ('vrc.v_sil', 'SIL', 'Silence'), + ('vrc.v_ss', 'SS', 'S as in "sit"'), + ('vrc.v_th', 'TH', 'Th as in "think"') + ], + update=lambda s, c: VisemePreview.update_preview(c) + ) eye_tracking_type: EnumProperty( - name=t("EyeTracking.type"), - description=t("EyeTracking.type_desc"), - items=[ - ('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")), - ('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc")) - ], - default='AV3' -) + name=t("EyeTracking.type"), + description=t("EyeTracking.type_desc"), + items=[ + ('AV3', t("EyeTracking.type.av3"), t("EyeTracking.type.av3_desc")), + ('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc")) + ], + default='AV3' + ) eye_mode: EnumProperty( name=t("EyeTracking.mode"), @@ -531,33 +542,100 @@ class AvatarToolkitSceneProperties(PropertyGroup): description=t("Tools.merge_twist_bones_desc"), default=True ) - + + highlight_problem_bones: BoolProperty( + name=t("Settings.highlight_problem_bones"), + description=t("Settings.highlight_problem_bones_desc"), + default=get_preference("highlight_problem_bones", True), + update=highlight_problem_bones + ) + + show_scale_issues: BoolProperty( + name="Show Scale Issues", + default=False + ) + + tpose_validation_result: BoolProperty( + name="T-Pose Validation Result", + default=True + ) + + tpose_validation_messages: CollectionProperty( + type=bpy.types.PropertyGroup, + name="T-Pose Validation Messages" + ) + + show_tpose_validation: BoolProperty( + name="Show T-Pose Validation Results", + default=False + ) + def register() -> None: """Register the Avatar Toolkit property group""" logger.info("Registering Avatar Toolkit properties") + + # Clear any existing registrations to prevent conflicts + if hasattr(bpy.types.Scene, "avatar_toolkit"): + try: + del bpy.types.Scene.avatar_toolkit + except: + logger.warning("Failed to remove existing avatar_toolkit property") + + # Register classes try: + # Try to register all classes at once bpy.utils.register_class(ZeroWeightBoneItem) + bpy.utils.register_class(ValidationMessageItem) bpy.utils.register_class(AvatarToolkitSceneProperties) - - - except ValueError: - # Class already registered, we can continue - pass + except ValueError as e: + logger.warning(f"Class registration issue: {e}") + # Try to unregister first in case they're already registered + try: + # Try to unregister in reverse order + try: + bpy.utils.unregister_class(AvatarToolkitSceneProperties) + except: + pass + try: + bpy.utils.unregister_class(ValidationMessageItem) + except: + pass + try: + bpy.utils.unregister_class(ZeroWeightBoneItem) + except: + pass + + # Then register again + bpy.utils.register_class(ZeroWeightBoneItem) + bpy.utils.register_class(ValidationMessageItem) + bpy.utils.register_class(AvatarToolkitSceneProperties) + except Exception as e: + logger.error(f"Failed to recover from registration error: {e}") + raise + + # Register the property bpy.types.Scene.avatar_toolkit = PointerProperty(type=AvatarToolkitSceneProperties) logger.debug("Properties registered successfully") + def unregister() -> None: """Unregister the Avatar Toolkit property group""" logger.info("Unregistering Avatar Toolkit properties") + + # Remove the property first + if hasattr(bpy.types.Scene, "avatar_toolkit"): + try: + del bpy.types.Scene.avatar_toolkit + logger.debug("Removed avatar_toolkit property") + except Exception as e: + logger.warning(f"Failed to remove avatar_toolkit property: {e}") + + # Then unregister the classes try: - del bpy.types.Scene.avatar_toolkit - except: - pass - try: - bpy.utils.unregister_class(ZeroWeightBoneItem) bpy.utils.unregister_class(AvatarToolkitSceneProperties) - - except RuntimeError: - pass - logger.debug("Properties unregistered successfully") - + bpy.utils.unregister_class(ValidationMessageItem) + bpy.utils.unregister_class(ZeroWeightBoneItem) + logger.debug("Unregistered property classes") + except (RuntimeError, ValueError) as e: + logger.warning(f"Error during property class unregistration: {e}") + # Not fatal - continue diff --git a/resources/translations/en_US.json b/resources/translations/en_US.json index 3d3ea15..bc67c62 100644 --- a/resources/translations/en_US.json +++ b/resources/translations/en_US.json @@ -50,6 +50,7 @@ "QuickAccess.validation_basic_details": "Only essential bone structure is being validated", "QuickAccess.validation_none_warning": "Validation Disabled", "QuickAccess.validation_none_details": "No armature validation checks are being performed", + "Quick_Access.import_success": "Import successful", "PoseMode.error.start": "Failed to start pose mode: {error}", "PoseMode.error.stop": "Failed to stop pose mode: {error}", @@ -78,6 +79,30 @@ "Validation.message.failed.line1": "Armature validation has failed", "Validation.message.failed.line2": "Please check below what the", "Validation.message.failed.line3": "issues are", + "Validation.highlight_problem_bones_desc": "Visually highlight bones that have validation issues in the viewport", + "Validation.no_armature": "No armature selected", + "Validation.no_issues": "No validation issues found to highlight", + "Validation.highlighting_complete": "Problem bones highlighted successfully", + "Validation.tpose.no_armature": "No armature found for T-pose validation", + "Validation.tpose.left_arm_not_horizontal": "Left arm is not in a horizontal T-pose position", + "Validation.tpose.right_arm_not_horizontal": "Right arm is not in a horizontal T-pose position", + "Validation.tpose.spine_not_vertical": "Spine is not in a vertical position", + "Validation.tpose.warning": "T-Pose Validation Warning", + "Validation.tpose.recommendation": "We recommend fixing the T-pose before importing into Unity or other platforms", + "Validation.scale_issues": "Bones with abnormal scale detected:", + "Validation.scale_issue.too_small": "Bone is extremely small", + "Validation.scale_issue.too_large": "Bone is extremely large", + "Validation.section.scale_issues": "Scale Issues", + "Validation.tpose.label": "Validate T-Pose", + "Validation.no_scale_issues": "No scale issues detected", + "Validation.no_hierarchy_issues": "No hierarchy issues detected", + "Validation.no_non_standard_issues": "No non-standard bone issues detected", + "Validation.tpose.valid": "T-Pose validation passed successfully", + "Validation.tpose.desc": "Check if armature is in a proper T-pose", + "Validation.highlight_problem_bones": "Highlight Problem Bones", + "Validation.clear_bone_highlighting": "Clear Bone Highlighting", + "Validation.clear_bone_highlighting_desc": "Remove bone highlighting and reset bone colors to default", + "Validation.highlighting_cleared": "Bone highlighting cleared successfully", "Mesh.validation.no_data": "No mesh data", "Mesh.validation.no_vertex_groups": "No vertex groups found", @@ -449,6 +474,9 @@ "Settings.enable_logging_desc": "Enable detailed debug logging for troubleshooting", "Settings.logging_enabled": "Debug logging enabled", "Settings.logging_disabled": "Debug logging disabled", + "Settings.highlight_problem_bones": "Highlight Problem Bones", + "Settings.highlight_problem_bones_desc": "Highlight bones with validation issues in the viewport", + "Settings.bone_highlighting": "Bone Highlighting", "Language.auto": "Automatic", "Language.en_US": "English", "Language.ja_JP": "Japanese", diff --git a/ui/quick_access_panel.py b/ui/quick_access_panel.py index 0d749e6..59fbd6d 100644 --- a/ui/quick_access_panel.py +++ b/ui/quick_access_panel.py @@ -84,7 +84,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): # Armature Validation active_armature: Optional[Object] = get_active_armature(context) if active_armature: - is_valid, messages, is_acceptable = validate_armature(active_armature) + is_valid, messages, is_acceptable, hierarchy_messages, scale_messages, non_standard_messages = validate_armature(active_armature, detailed_messages=True) info_box = col.box() @@ -121,23 +121,70 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel): 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) + 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'): + if non_standard_messages: + for message in non_standard_messages: + for line in message.split('\n'): + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=line) + else: sub_row = validation_box.row() - sub_row.alert = True - sub_row.label(text=line) + sub_row.label(text=t("Validation.no_non_standard_issues")) # 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) + 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:]: + if hierarchy_messages: + for message in hierarchy_messages: + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=message) + else: sub_row = validation_box.row() - sub_row.alert = True - sub_row.label(text=message) + sub_row.label(text=t("Validation.no_hierarchy_issues")) + + # Scale Issues section + validation_box = info_box.box() + row = validation_box.row() + row.alert = True + row.prop(props, "show_scale_issues", text=t("Validation.section.scale_issues"), + icon='TRIA_DOWN' if props.show_scale_issues else 'TRIA_RIGHT', emboss=False) + if props.show_scale_issues: + if scale_messages: + for scale_msg in scale_messages: + sub_row = validation_box.row() + sub_row.alert = True + sub_row.label(text=scale_msg) + else: + sub_row = validation_box.row() + sub_row.label(text=t("Validation.no_scale_issues")) + + pose_box = layout.box() + col = pose_box.column(align=True) + col.label(text=t("Validation.tpose.label"), icon='ARMATURE_DATA') + col.separator(factor=0.5) + col.operator("avatar_toolkit.validate_tpose", icon='CHECKMARK') + + if props.show_tpose_validation: + validation_box = col.box() + if props.tpose_validation_result: + validation_box.label(text=t("Validation.tpose.valid"), icon='CHECKMARK') + else: + row = validation_box.row() + row.alert = True + row.label(text=t("Validation.tpose.warning"), icon='ERROR') + + for msg in props.tpose_validation_messages: + row = validation_box.row() + row.alert = True + row.label(text=msg.name) else: # If no specific issues, show acceptable message info_box.label(text=messages[0], icon='INFO') diff --git a/ui/settings_panel.py b/ui/settings_panel.py index ed32263..d0bd2f3 100644 --- a/ui/settings_panel.py +++ b/ui/settings_panel.py @@ -42,6 +42,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel): def draw(self, context: Context) -> None: """Draw the settings panel layout with language selection""" layout: UILayout = self.layout + props = context.scene.avatar_toolkit # Language Settings lang_box: UILayout = layout.box() @@ -50,7 +51,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel): row.scale_y = 1.2 row.label(text=t("Settings.language"), icon='WORLD') col.separator() - col.prop(context.scene.avatar_toolkit, "language", text="") + col.prop(props, "language", text="") # Validation Settings val_box: UILayout = layout.box() @@ -59,18 +60,31 @@ class AvatarToolKit_PT_SettingsPanel(Panel): row.scale_y = 1.2 row.label(text=t("Settings.validation_mode"), icon='CHECKMARK') col.separator() - col.prop(context.scene.avatar_toolkit, "validation_mode", text="") + col.prop(props, "validation_mode", text="") + + # Bone Highlighting Settings + bone_box: UILayout = layout.box() + col = bone_box.column(align=True) + row = col.row() + row.scale_y = 1.2 + row.label(text=t("Settings.bone_highlighting"), icon='BONE_DATA') + col.separator() + col.prop(props, "highlight_problem_bones") + if props.highlight_problem_bones: + col.operator("avatar_toolkit.highlight_problem_bones", icon='COLOR') + else: + col.operator("avatar_toolkit.clear_bone_highlighting", icon='X') # Debug Settings debug_box = layout.box() col = debug_box.column() row = col.row(align=True) - row.prop(context.scene.avatar_toolkit, "debug_expand", - icon="TRIA_DOWN" if context.scene.avatar_toolkit.debug_expand + row.prop(props, "debug_expand", + icon="TRIA_DOWN" if props.debug_expand else "TRIA_RIGHT", icon_only=True, emboss=False) row.label(text=t("Settings.debug"), icon='CONSOLE') - if context.scene.avatar_toolkit.debug_expand: + if props.debug_expand: col = debug_box.column(align=True) - col.prop(context.scene.avatar_toolkit, "enable_logging") + col.prop(props, "enable_logging")