Merge pull request #124 from Yusarina/Amrature-Validation-P2
Armature Validation P2
This commit is contained in:
+15
@@ -18,8 +18,23 @@ def register():
|
|||||||
|
|
||||||
from .core import auto_load
|
from .core import auto_load
|
||||||
print("Starting registration")
|
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()
|
auto_load.init()
|
||||||
|
|
||||||
|
# Register classes in proper order
|
||||||
auto_load.register()
|
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")
|
print("Registration complete")
|
||||||
|
|
||||||
def unregister():
|
def unregister():
|
||||||
|
|||||||
@@ -60,3 +60,4 @@ if not os.path.exists(PREFERENCES_FILE):
|
|||||||
save_preference("language", 0) # Set default language to 0 (auto)
|
save_preference("language", 0) # Set default language to 0 (auto)
|
||||||
save_preference("validation_mode", "STRICT") # Set default validation mode
|
save_preference("validation_mode", "STRICT") # Set default validation mode
|
||||||
save_preference("enable_logging", False) # Set default logging mode
|
save_preference("enable_logging", False) # Set default logging mode
|
||||||
|
save_preference("highlight_problem_bones", True) # Set default bone highlighting
|
||||||
+397
-6
@@ -1,6 +1,9 @@
|
|||||||
import bpy
|
import bpy
|
||||||
from typing import Tuple, List, Dict, Set, Optional
|
import math
|
||||||
from bpy.types import Object, Bone
|
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.translations import t
|
||||||
from ..core.dictionaries import (
|
from ..core.dictionaries import (
|
||||||
standard_bones,
|
standard_bones,
|
||||||
@@ -9,23 +12,35 @@ from ..core.dictionaries import (
|
|||||||
acceptable_bone_hierarchy,
|
acceptable_bone_hierarchy,
|
||||||
acceptable_bone_names
|
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
|
validation_mode = bpy.context.scene.avatar_toolkit.validation_mode
|
||||||
messages: List[str] = []
|
messages: List[str] = []
|
||||||
hierarchy_messages: List[str] = []
|
hierarchy_messages: List[str] = []
|
||||||
non_standard_messages: List[str] = []
|
non_standard_messages: List[str] = []
|
||||||
|
scale_messages: List[str] = []
|
||||||
|
|
||||||
if validation_mode == 'NONE':
|
if validation_mode == 'NONE':
|
||||||
|
logger.debug("Validation mode is NONE, skipping validation")
|
||||||
|
if detailed_messages:
|
||||||
|
return True, [], False, [], [], []
|
||||||
|
else:
|
||||||
return True, [], False
|
return True, [], False
|
||||||
|
|
||||||
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
|
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
|
||||||
|
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
|
return False, [t("Armature.validation.basic_check_failed")], False
|
||||||
|
|
||||||
found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones}
|
found_bones: Dict[str, Bone] = {bone.name: bone for bone in armature.data.bones}
|
||||||
|
logger.debug(f"Found {len(found_bones)} bones in armature")
|
||||||
is_acceptable = check_acceptable_standards(found_bones)
|
is_acceptable = check_acceptable_standards(found_bones)
|
||||||
|
|
||||||
# List all bones in armature
|
# List all bones in armature
|
||||||
@@ -39,31 +54,46 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]:
|
|||||||
|
|
||||||
if missing_bones:
|
if missing_bones:
|
||||||
missing_list = "\n".join([f"- {bone}" for bone in 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))
|
hierarchy_messages.append(t("Armature.validation.missing_bones", bones=missing_list))
|
||||||
|
|
||||||
if validation_mode == 'STRICT':
|
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
|
# Validate bone hierarchy
|
||||||
for parent, child in bone_hierarchy:
|
for parent, child in bone_hierarchy:
|
||||||
if parent in found_bones and child in found_bones:
|
if parent in found_bones and child in found_bones:
|
||||||
if not validate_bone_hierarchy(found_bones, parent, child):
|
if not validate_bone_hierarchy(found_bones, parent, child):
|
||||||
|
logger.warning(f"Invalid hierarchy: {parent} -> {child}")
|
||||||
hierarchy_messages.append(t("Armature.validation.invalid_hierarchy",
|
hierarchy_messages.append(t("Armature.validation.invalid_hierarchy",
|
||||||
parent=parent, child=child))
|
parent=parent, child=child))
|
||||||
|
|
||||||
# Validate symmetry
|
# Validate symmetry
|
||||||
|
logger.debug("Validating bone symmetry")
|
||||||
symmetry_pairs = [('arm', 'L', 'R'), ('leg', 'L', 'R')]
|
symmetry_pairs = [('arm', 'L', 'R'), ('leg', 'L', 'R')]
|
||||||
for base, left, right in symmetry_pairs:
|
for base, left, right in symmetry_pairs:
|
||||||
if not validate_symmetry(found_bones, base, left, right):
|
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))
|
hierarchy_messages.append(t("Armature.validation.asymmetric_bones", bone=base))
|
||||||
|
|
||||||
if (not validate_symmetry(found_bones, 'hand', 'L', 'R') and
|
if (not validate_symmetry(found_bones, 'hand', 'L', 'R') and
|
||||||
not validate_symmetry(found_bones, 'wrist', 'L', 'R')):
|
not validate_symmetry(found_bones, 'wrist', 'L', 'R')):
|
||||||
|
logger.warning("Asymmetric hand/wrist bones found")
|
||||||
hierarchy_messages.append(t("Armature.validation.asymmetric_hand_wrist"))
|
hierarchy_messages.append(t("Armature.validation.asymmetric_hand_wrist"))
|
||||||
|
|
||||||
# Validate finger hierarchies
|
# Validate finger hierarchies
|
||||||
|
logger.debug("Validating finger hierarchies")
|
||||||
for side in ['left', 'right']:
|
for side in ['left', 'right']:
|
||||||
for finger_chain in finger_hierarchy[side]:
|
for finger_chain in finger_hierarchy[side]:
|
||||||
if all(bone in found_bones for bone in finger_chain):
|
if all(bone in found_bones for bone in finger_chain):
|
||||||
if not validate_finger_chain(found_bones, 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]))
|
hierarchy_messages.append(t("Armature.validation.invalid_finger", finger=finger_chain[0]))
|
||||||
|
|
||||||
# Non-standard bones check
|
# Non-standard bones check
|
||||||
@@ -83,26 +113,38 @@ def validate_armature(armature: Object) -> Tuple[bool, List[str], bool]:
|
|||||||
non_standard_bones.append(bone_name)
|
non_standard_bones.append(bone_name)
|
||||||
|
|
||||||
if non_standard_bones:
|
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_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))
|
non_standard_messages.append(t("Armature.validation.non_standard_bones", bones=non_standard_list))
|
||||||
|
|
||||||
# Combine messages in correct order
|
# Combine messages in correct order
|
||||||
messages.extend(non_standard_messages)
|
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 not is_valid and is_acceptable:
|
||||||
if non_standard_bones:
|
if non_standard_bones:
|
||||||
|
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
|
return False, messages, False
|
||||||
|
|
||||||
|
logger.info("Armature meets acceptable standards")
|
||||||
messages = [
|
messages = [
|
||||||
t("Armature.validation.acceptable_standard.success"),
|
t("Armature.validation.acceptable_standard.success"),
|
||||||
t("Armature.validation.acceptable_standard.note"),
|
t("Armature.validation.acceptable_standard.note"),
|
||||||
t("Armature.validation.acceptable_standard.option")
|
t("Armature.validation.acceptable_standard.option")
|
||||||
]
|
]
|
||||||
|
if detailed_messages:
|
||||||
|
return True, messages, True, [], [], []
|
||||||
|
else:
|
||||||
return True, messages, True
|
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
|
return is_valid, messages, False
|
||||||
|
|
||||||
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
|
def validate_bone_hierarchy(bones: Dict[str, Bone], parent_name: str, child_name: str) -> bool:
|
||||||
@@ -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:
|
def check_acceptable_standards(bones: Dict[str, Bone]) -> bool:
|
||||||
"""Check if armature matches acceptable non-standard hierarchy"""
|
"""Check if armature matches acceptable non-standard hierarchy"""
|
||||||
|
logger.debug("Checking for acceptable standards")
|
||||||
# Check if bones exist in acceptable list
|
# Check if bones exist in acceptable list
|
||||||
for bone_category, acceptable_names in acceptable_bone_names.items():
|
for bone_category, acceptable_names in acceptable_bone_names.items():
|
||||||
found = False
|
found = False
|
||||||
@@ -156,12 +199,360 @@ def check_acceptable_standards(bones: Dict[str, Bone]) -> bool:
|
|||||||
found = True
|
found = True
|
||||||
break
|
break
|
||||||
if not found:
|
if not found:
|
||||||
|
logger.debug(f"Missing acceptable bone for category: {bone_category}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Validate acceptable hierarchy
|
# Validate acceptable hierarchy
|
||||||
for parent, child in acceptable_bone_hierarchy:
|
for parent, child in acceptable_bone_hierarchy:
|
||||||
if parent in bones and child in bones:
|
if parent in bones and child in bones:
|
||||||
if not validate_bone_hierarchy(bones, parent, child):
|
if not validate_bone_hierarchy(bones, parent, child):
|
||||||
|
logger.debug(f"Invalid acceptable hierarchy: {parent} -> {child}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
logger.debug("Armature meets acceptable standards")
|
||||||
return True
|
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'}
|
||||||
|
|||||||
+68
-23
@@ -498,32 +498,77 @@ acceptable_bone_hierarchy = [
|
|||||||
('Head', 'Eye_L'),
|
('Head', 'Eye_L'),
|
||||||
('Head', 'Eye_R'),
|
('Head', 'Eye_R'),
|
||||||
('Head', 'LeftEye'),
|
('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 = {
|
acceptable_bone_names = {
|
||||||
'hips': ['Hips'],
|
'hips': ['Hips', 'pelvis', 'root', 'Root', 'ROOT'],
|
||||||
'chest': ['Chest'],
|
'chest': ['Chest', 'spine1', 'Spine1', 'spine_01', 'SPINE1', 'Spine01'],
|
||||||
'neck': ['Neck'],
|
'neck': ['Neck', 'neck_01', 'Neck01'],
|
||||||
'head': ['Head'],
|
'head': ['Head', 'head_01', 'Head01'],
|
||||||
'eye_l': ['Eye_L', 'LeftEye'],
|
'eye_l': ['Eye_L', 'LeftEye', 'lefteye', 'eye_left', 'EyeLeft'],
|
||||||
'eye_r': ['Eye_R', 'RightEye'],
|
'eye_r': ['Eye_R', 'RightEye', 'righteye', 'eye_right', 'EyeRight'],
|
||||||
'shoulder_r': ['Shoulder.R'],
|
|
||||||
'arm_r': ['Arm.R'],
|
'shoulder_r': ['Shoulder.R', 'clavicle_r', 'ClavicleRight', 'RightShoulder'],
|
||||||
'elbow_r': ['Elbow.R'],
|
'arm_r': ['Arm.R', 'upperarm_r', 'UpperArmRight', 'RightArm'],
|
||||||
'wrist_r': ['Wrist.R'],
|
'elbow_r': ['Elbow.R', 'lowerarm_r', 'ForearmRight', 'RightForeArm'],
|
||||||
'leg_r': ['Leg.R'],
|
'wrist_r': ['Wrist.R', 'hand_r', 'HandRight', 'RightHand'],
|
||||||
'knee_r': ['Knee.R'],
|
'leg_r': ['Leg.R', 'thigh_r', 'ThighRight', 'RightLeg', 'RightUpLeg'],
|
||||||
'foot_r': ['Foot.R'],
|
'knee_r': ['Knee.R', 'calf_r', 'CalfRight', 'RightShin', 'RightLowerLeg'],
|
||||||
'toes_r': ['Toes.R'],
|
'foot_r': ['Foot.R', 'foot_r', 'FootRight', 'RightFoot'],
|
||||||
'shoulder_l': ['Shoulder.L'],
|
'toes_r': ['Toes.R', 'ball_r', 'ToeRight', 'RightToeBase'],
|
||||||
'arm_l': ['Arm.L'],
|
|
||||||
'elbow_l': ['Elbow.L'],
|
'shoulder_l': ['Shoulder.L', 'clavicle_l', 'ClavicleLeft', 'LeftShoulder'],
|
||||||
'wrist_l': ['Wrist.L'],
|
'arm_l': ['Arm.L', 'upperarm_l', 'UpperArmLeft', 'LeftArm'],
|
||||||
'leg_l': ['Leg.L'],
|
'elbow_l': ['Elbow.L', 'lowerarm_l', 'ForearmLeft', 'LeftForeArm'],
|
||||||
'knee_l': ['Knee.L'],
|
'wrist_l': ['Wrist.L', 'hand_l', 'HandLeft', 'LeftHand'],
|
||||||
'foot_l': ['Foot.L'],
|
'leg_l': ['Leg.L', 'thigh_l', 'ThighLeft', 'LeftLeg', 'LeftUpLeg'],
|
||||||
'toes_l': ['Toes.L']
|
'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 = {
|
rigify_unity_names = {
|
||||||
|
|||||||
@@ -36,3 +36,10 @@ def update_logging_state(self: Any, context: Context) -> None:
|
|||||||
enabled = self.enable_logging
|
enabled = self.enable_logging
|
||||||
save_preference("enable_logging", enabled)
|
save_preference("enable_logging", enabled)
|
||||||
configure_logging(enabled)
|
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'}")
|
||||||
|
|||||||
+100
-22
@@ -18,6 +18,10 @@ from .common import get_armature_list, get_active_armature, get_all_meshes, Scen
|
|||||||
from ..functions.visemes import VisemePreview
|
from ..functions.visemes import VisemePreview
|
||||||
from ..functions.eye_tracking import set_rotation
|
from ..functions.eye_tracking import set_rotation
|
||||||
|
|
||||||
|
class ValidationMessageItem(PropertyGroup):
|
||||||
|
"""Property group for validation message items"""
|
||||||
|
name: StringProperty(name="Message")
|
||||||
|
|
||||||
class ZeroWeightBoneItem(PropertyGroup):
|
class ZeroWeightBoneItem(PropertyGroup):
|
||||||
"""Property group for zero weight bone list items"""
|
"""Property group for zero weight bone list items"""
|
||||||
name: StringProperty(name="Bone Name")
|
name: StringProperty(name="Bone Name")
|
||||||
@@ -25,11 +29,13 @@ class ZeroWeightBoneItem(PropertyGroup):
|
|||||||
has_children: BoolProperty(name="Has Children", default=False)
|
has_children: BoolProperty(name="Has Children", default=False)
|
||||||
is_deform: BoolProperty(name="Is Deform Bone", default=False)
|
is_deform: BoolProperty(name="Is Deform Bone", default=False)
|
||||||
|
|
||||||
|
|
||||||
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
def update_validation_mode(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates validation mode and saves preference"""
|
"""Updates validation mode and saves preference"""
|
||||||
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
logger.info(f"Updating validation mode to: {self.validation_mode}")
|
||||||
save_preference("validation_mode", self.validation_mode)
|
save_preference("validation_mode", self.validation_mode)
|
||||||
|
|
||||||
|
|
||||||
def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
def update_logging_state(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates logging state and configures logging"""
|
"""Updates logging state and configures logging"""
|
||||||
logger.info(f"Updating logging state to: {self.enable_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
|
from .logging_setup import configure_logging
|
||||||
configure_logging(self.enable_logging)
|
configure_logging(self.enable_logging)
|
||||||
|
|
||||||
|
|
||||||
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
|
def update_shape_intensity(self: PropertyGroup, context: Context) -> None:
|
||||||
"""Updates shape key intensity and refreshes preview"""
|
"""Updates shape key intensity and refreshes preview"""
|
||||||
if self.viseme_preview_mode:
|
if self.viseme_preview_mode:
|
||||||
VisemePreview.update_preview(context)
|
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):
|
def get_mesh_objects(self, context):
|
||||||
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
|
meshes = [(obj.name, obj.name, "") for obj in bpy.data.objects if obj.type == 'MESH']
|
||||||
if not meshes:
|
if not meshes:
|
||||||
@@ -76,8 +88,8 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
if self.use_nodes:
|
if self.use_nodes:
|
||||||
Object.Enum = [((i.image.name if i.image else i.name+"_image"),
|
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 "node with no image..."),
|
||||||
(i.image.name if i.image else i.name),index+1)
|
(i.image.name if i.image else i.name), index+1)
|
||||||
for index,i in enumerate(self.node_tree.nodes)
|
for index, i in enumerate(self.node_tree.nodes)
|
||||||
if i.bl_idname == "ShaderNodeTexImage"]
|
if i.bl_idname == "ShaderNodeTexImage"]
|
||||||
if not len(Object.Enum):
|
if not len(Object.Enum):
|
||||||
Object.Enum = [(t("TextureAtlas.error.label"),
|
Object.Enum = [(t("TextureAtlas.error.label"),
|
||||||
@@ -318,8 +330,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
('vrc.v_th', 'TH', 'Th as in "think"')
|
('vrc.v_th', 'TH', 'Th as in "think"')
|
||||||
],
|
],
|
||||||
update=lambda s, c: VisemePreview.update_preview(c)
|
update=lambda s, c: VisemePreview.update_preview(c)
|
||||||
|
)
|
||||||
)
|
|
||||||
|
|
||||||
eye_tracking_type: EnumProperty(
|
eye_tracking_type: EnumProperty(
|
||||||
name=t("EyeTracking.type"),
|
name=t("EyeTracking.type"),
|
||||||
@@ -329,7 +340,7 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
|
('SDK2', t("EyeTracking.type.sdk2"), t("EyeTracking.type.sdk2_desc"))
|
||||||
],
|
],
|
||||||
default='AV3'
|
default='AV3'
|
||||||
)
|
)
|
||||||
|
|
||||||
eye_mode: EnumProperty(
|
eye_mode: EnumProperty(
|
||||||
name=t("EyeTracking.mode"),
|
name=t("EyeTracking.mode"),
|
||||||
@@ -532,32 +543,99 @@ class AvatarToolkitSceneProperties(PropertyGroup):
|
|||||||
default=True
|
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:
|
def register() -> None:
|
||||||
"""Register the Avatar Toolkit property group"""
|
"""Register the Avatar Toolkit property group"""
|
||||||
logger.info("Registering Avatar Toolkit properties")
|
logger.info("Registering Avatar Toolkit properties")
|
||||||
try:
|
|
||||||
bpy.utils.register_class(ZeroWeightBoneItem)
|
|
||||||
bpy.utils.register_class(AvatarToolkitSceneProperties)
|
|
||||||
|
|
||||||
|
# Clear any existing registrations to prevent conflicts
|
||||||
except ValueError:
|
if hasattr(bpy.types.Scene, "avatar_toolkit"):
|
||||||
# Class already registered, we can continue
|
|
||||||
pass
|
|
||||||
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")
|
|
||||||
try:
|
try:
|
||||||
del bpy.types.Scene.avatar_toolkit
|
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 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:
|
except:
|
||||||
pass
|
pass
|
||||||
try:
|
try:
|
||||||
bpy.utils.unregister_class(ZeroWeightBoneItem)
|
bpy.utils.unregister_class(ZeroWeightBoneItem)
|
||||||
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
|
except:
|
||||||
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
pass
|
||||||
logger.debug("Properties unregistered successfully")
|
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
bpy.utils.unregister_class(AvatarToolkitSceneProperties)
|
||||||
|
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
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"QuickAccess.validation_basic_details": "Only essential bone structure is being validated",
|
"QuickAccess.validation_basic_details": "Only essential bone structure is being validated",
|
||||||
"QuickAccess.validation_none_warning": "Validation Disabled",
|
"QuickAccess.validation_none_warning": "Validation Disabled",
|
||||||
"QuickAccess.validation_none_details": "No armature validation checks are being performed",
|
"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.start": "Failed to start pose mode: {error}",
|
||||||
"PoseMode.error.stop": "Failed to stop 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.line1": "Armature validation has failed",
|
||||||
"Validation.message.failed.line2": "Please check below what the",
|
"Validation.message.failed.line2": "Please check below what the",
|
||||||
"Validation.message.failed.line3": "issues are",
|
"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_data": "No mesh data",
|
||||||
"Mesh.validation.no_vertex_groups": "No vertex groups found",
|
"Mesh.validation.no_vertex_groups": "No vertex groups found",
|
||||||
@@ -449,6 +474,9 @@
|
|||||||
"Settings.enable_logging_desc": "Enable detailed debug logging for troubleshooting",
|
"Settings.enable_logging_desc": "Enable detailed debug logging for troubleshooting",
|
||||||
"Settings.logging_enabled": "Debug logging enabled",
|
"Settings.logging_enabled": "Debug logging enabled",
|
||||||
"Settings.logging_disabled": "Debug logging disabled",
|
"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.auto": "Automatic",
|
||||||
"Language.en_US": "English",
|
"Language.en_US": "English",
|
||||||
"Language.ja_JP": "Japanese",
|
"Language.ja_JP": "Japanese",
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
# Armature Validation
|
# Armature Validation
|
||||||
active_armature: Optional[Object] = get_active_armature(context)
|
active_armature: Optional[Object] = get_active_armature(context)
|
||||||
if active_armature:
|
if active_armature:
|
||||||
is_valid, messages, 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()
|
info_box = col.box()
|
||||||
|
|
||||||
@@ -121,23 +121,70 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
|||||||
validation_box = info_box.box()
|
validation_box = info_box.box()
|
||||||
row = validation_box.row()
|
row = validation_box.row()
|
||||||
row.alert = True
|
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:
|
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 = validation_box.row()
|
||||||
sub_row.alert = True
|
sub_row.alert = True
|
||||||
sub_row.label(text=line)
|
sub_row.label(text=line)
|
||||||
|
else:
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
sub_row.label(text=t("Validation.no_non_standard_issues"))
|
||||||
|
|
||||||
# Hierarchy Issues section
|
# Hierarchy Issues section
|
||||||
validation_box = info_box.box()
|
validation_box = info_box.box()
|
||||||
row = validation_box.row()
|
row = validation_box.row()
|
||||||
row.alert = True
|
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:
|
if props.show_hierarchy:
|
||||||
for message in messages[2:]:
|
if hierarchy_messages:
|
||||||
|
for message in hierarchy_messages:
|
||||||
sub_row = validation_box.row()
|
sub_row = validation_box.row()
|
||||||
sub_row.alert = True
|
sub_row.alert = True
|
||||||
sub_row.label(text=message)
|
sub_row.label(text=message)
|
||||||
|
else:
|
||||||
|
sub_row = validation_box.row()
|
||||||
|
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:
|
else:
|
||||||
# If no specific issues, show acceptable message
|
# If no specific issues, show acceptable message
|
||||||
info_box.label(text=messages[0], icon='INFO')
|
info_box.label(text=messages[0], icon='INFO')
|
||||||
|
|||||||
+20
-6
@@ -42,6 +42,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
def draw(self, context: Context) -> None:
|
def draw(self, context: Context) -> None:
|
||||||
"""Draw the settings panel layout with language selection"""
|
"""Draw the settings panel layout with language selection"""
|
||||||
layout: UILayout = self.layout
|
layout: UILayout = self.layout
|
||||||
|
props = context.scene.avatar_toolkit
|
||||||
|
|
||||||
# Language Settings
|
# Language Settings
|
||||||
lang_box: UILayout = layout.box()
|
lang_box: UILayout = layout.box()
|
||||||
@@ -50,7 +51,7 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
row.scale_y = 1.2
|
row.scale_y = 1.2
|
||||||
row.label(text=t("Settings.language"), icon='WORLD')
|
row.label(text=t("Settings.language"), icon='WORLD')
|
||||||
col.separator()
|
col.separator()
|
||||||
col.prop(context.scene.avatar_toolkit, "language", text="")
|
col.prop(props, "language", text="")
|
||||||
|
|
||||||
# Validation Settings
|
# Validation Settings
|
||||||
val_box: UILayout = layout.box()
|
val_box: UILayout = layout.box()
|
||||||
@@ -59,18 +60,31 @@ class AvatarToolKit_PT_SettingsPanel(Panel):
|
|||||||
row.scale_y = 1.2
|
row.scale_y = 1.2
|
||||||
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
|
row.label(text=t("Settings.validation_mode"), icon='CHECKMARK')
|
||||||
col.separator()
|
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 Settings
|
||||||
debug_box = layout.box()
|
debug_box = layout.box()
|
||||||
col = debug_box.column()
|
col = debug_box.column()
|
||||||
row = col.row(align=True)
|
row = col.row(align=True)
|
||||||
row.prop(context.scene.avatar_toolkit, "debug_expand",
|
row.prop(props, "debug_expand",
|
||||||
icon="TRIA_DOWN" if context.scene.avatar_toolkit.debug_expand
|
icon="TRIA_DOWN" if props.debug_expand
|
||||||
else "TRIA_RIGHT",
|
else "TRIA_RIGHT",
|
||||||
icon_only=True, emboss=False)
|
icon_only=True, emboss=False)
|
||||||
row.label(text=t("Settings.debug"), icon='CONSOLE')
|
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 = debug_box.column(align=True)
|
||||||
col.prop(context.scene.avatar_toolkit, "enable_logging")
|
col.prop(props, "enable_logging")
|
||||||
|
|||||||
Reference in New Issue
Block a user