Pose mode improvements, armature validation improvements.

Pose mode Improvements:

Batch processing for all mesh operations
Numpy-powered vertex array handling
Optimized modifier stack management
Smart shape key processing
Enhanced progress tracking

The armature validation system improvements:

Essential bones (hips, spine, chest, neck, head)
Proper bone hierarchy validation
Symmetry pair verification (e.g., arm.l/arm.r)
This commit is contained in:
Yusarina
2024-12-04 00:54:21 +00:00
parent ff23d23cfc
commit 5dcaba381d
4 changed files with 370 additions and 150 deletions
+192 -39
View File
@@ -1,10 +1,48 @@
import bpy import bpy
import numpy as np import numpy as np
from bpy.types import Context, Object import logging
from typing import Optional, Tuple, List, Set from bpy.types import Context, Object, Modifier
from typing import Optional, Tuple, List, Set, Dict, Any, Generator, Callable
from ..core.translations import t from ..core.translations import t
from ..core.dictionaries import bone_names from ..core.dictionaries import bone_names
logger = logging.getLogger('avatar_toolkit')
logger.setLevel(logging.DEBUG)
def setup_logging() -> None:
"""Configure logging for Avatar Toolkit"""
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
class ProgressTracker:
"""Universal progress tracking for Avatar Toolkit operations"""
def __init__(self, context: Context, total_steps: int, operation_name: str = "Operation"):
self.context = context
self.total = total_steps
self.current = 0
self.operation_name = operation_name
self.wm = context.window_manager
def step(self, message: str = "") -> None:
"""Update progress by one step"""
self.current += 1
progress = self.current / self.total
self.wm.progress_begin(0, 100)
self.wm.progress_update(progress * 100)
logger.debug(f"{self.operation_name} - {progress:.1%}: {message}")
def __enter__(self):
logger.info(f"Starting {self.operation_name}")
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.wm.progress_end()
logger.info(f"Completed {self.operation_name}")
def get_active_armature(context: bpy.types.Context) -> Optional[bpy.types.Object]: def get_active_armature(context: bpy.types.Context) -> Optional[bpy.types.Object]:
"""Get the currently selected armature from Avatar Toolkit properties""" """Get the currently selected armature from Avatar Toolkit properties"""
armature_name = context.scene.avatar_toolkit.active_armature armature_name = context.scene.avatar_toolkit.active_armature
@@ -25,27 +63,95 @@ def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tupl
return [('NONE', t("Armature.validation.no_armature"), '')] return [('NONE', t("Armature.validation.no_armature"), '')]
return armatures return armatures
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, str]: def validate_armature(armature: bpy.types.Object, validation_level: str = 'standard') -> Tuple[bool, List[str]]:
""" """Enhanced armature validation with multiple checks and validation levels"""
Validate if the selected object is a proper armature and has required bones messages = []
Returns tuple of (is_valid, message)
"""
if not armature:
return False, t("Armature.validation.no_armature")
if armature.type != 'ARMATURE':
return False, t("Armature.validation.not_armature")
if not armature.data.bones:
return False, t("Armature.validation.no_bones")
essential_bones: Set[str] = {'hips', 'spine', 'chest', 'neck', 'head'}
found_bones: Set[str] = {bone.name.lower() for bone in armature.data.bones}
# Basic checks
if not armature or armature.type != 'ARMATURE' or not armature.data.bones:
return False, [t("Armature.validation.basic_check_failed")]
found_bones = {bone.name.lower(): bone for bone in armature.data.bones}
# Essential bones check
essential_bones = {'hips', 'spine', 'chest', 'neck', 'head'}
missing_bones = []
for bone in essential_bones: for bone in essential_bones:
if not any(alt_name in found_bones for alt_name in bone_names[bone]): if not any(alt_name in found_bones for alt_name in bone_names[bone]):
return False, t("Armature.validation.missing_bone", bone=bone) missing_bones.append(bone)
return True, t("QuickAccess.valid_armature") if missing_bones:
messages.append(t("Armature.validation.missing_bones", bones=", ".join(missing_bones)))
if validation_level in ['standard', 'strict']:
# Hierarchy validation
hierarchy = [('hips', 'spine'), ('spine', 'chest'), ('chest', 'neck'), ('neck', 'head')]
for parent, child in hierarchy:
if not validate_bone_hierarchy(found_bones, parent, child):
messages.append(t("Armature.validation.invalid_hierarchy", parent=parent, child=child))
# Symmetry validation
symmetry_pairs = [('arm', 'l', 'r'), ('leg', 'l', 'r')]
for base, left, right in symmetry_pairs:
if not validate_symmetry(found_bones, base, left, right):
messages.append(t("Armature.validation.asymmetric_bones", bone=base))
# Special handling for hand/wrist symmetry
if (not validate_symmetry(found_bones, 'hand', 'l', 'r') and
not validate_symmetry(found_bones, 'wrist', 'l', 'r')):
messages.append(t("Armature.validation.asymmetric_hand_wrist"))
is_valid = len(messages) == 0
return is_valid, messages
def validate_bone_hierarchy(bones: Dict[str, bpy.types.Bone], parent_name: str, child_name: str) -> bool:
"""Validate if there is a valid parent-child relationship between bones"""
# Find matching parent and child bones using bone_names dictionary
parent_bone = None
child_bone = None
# Check for parent bone matches
for alt_name in bone_names[parent_name]:
if alt_name in bones:
parent_bone = bones[alt_name]
break
# Check for child bone matches
for alt_name in bone_names[child_name]:
if alt_name in bones:
child_bone = bones[alt_name]
break
if not parent_bone or not child_bone:
return False
# Check if child's parent matches parent bone
return child_bone.parent == parent_bone
def validate_symmetry(bones: Dict[str, bpy.types.Bone], base: str, left: str, right: str) -> bool:
"""
Validate if matching left and right bones exist for a given base bone name
"""
# Define common naming patterns
left_patterns = [
f"{base}.{left}",
f"{base}_{left}",
f"{left}_{base}"
]
right_patterns = [
f"{base}.{right}",
f"{base}_{right}",
f"{right}_{base}"
]
# Check if any of the patterns exist in the bones dictionary
left_exists = any(pattern in bones for pattern in left_patterns)
right_exists = any(pattern in bones for pattern in right_patterns)
return left_exists and right_exists
def auto_select_single_armature(context: bpy.types.Context) -> None: def auto_select_single_armature(context: bpy.types.Context) -> None:
"""Automatically select armature if only one exists in scene""" """Automatically select armature if only one exists in scene"""
armatures = get_armature_list(context) armatures = get_armature_list(context)
@@ -69,33 +175,79 @@ def get_armature_stats(armature: bpy.types.Object) -> dict:
} }
def get_all_meshes(context: Context) -> List[Object]: def get_all_meshes(context: Context) -> List[Object]:
"""Get all mesh objects parented to the active armature"""
armature = get_active_armature(context) armature = get_active_armature(context)
if armature: if armature:
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature] return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
return [] return []
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Object]) -> bool: def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
for mesh_obj in meshes: """Validate mesh object for pose operations"""
if not mesh_obj.data: if not mesh_obj.data:
continue return False, t("Mesh.validation.no_data")
if mesh_obj.data.shape_keys and mesh_obj.data.shape_keys.key_blocks:
if len(mesh_obj.data.shape_keys.key_blocks) == 1: if not mesh_obj.vertex_groups:
basis = mesh_obj.data.shape_keys.key_blocks[0] return False, t("Mesh.validation.no_vertex_groups")
basis_name = basis.name
mesh_obj.shape_key_remove(basis) armature_mods = [mod for mod in mesh_obj.modifiers if mod.type == 'ARMATURE']
apply_armature_to_mesh(armature_obj, mesh_obj) if not armature_mods:
mesh_obj.shape_key_add(name=basis_name) return False, t("Mesh.validation.no_armature_modifier")
else:
apply_armature_to_mesh_with_shapekeys(armature_obj, mesh_obj, context) return True, t("Mesh.validation.valid")
else:
apply_armature_to_mesh(armature_obj, mesh_obj) def cache_vertex_positions(mesh_obj: Object) -> np.ndarray:
"""Cache vertex positions for a mesh object"""
vertices = mesh_obj.data.vertices
positions = np.empty(len(vertices) * 3, dtype=np.float32)
vertices.foreach_get('co', positions)
return positions.reshape(-1, 3)
def apply_vertex_positions(vertices: Object, positions: np.ndarray) -> None:
"""Apply cached vertex positions to mesh in batch"""
vertices.foreach_set('co', positions.flatten())
def process_armature_modifiers(mesh_obj: Object) -> List[Dict[str, Any]]:
"""Process and store armature modifier states"""
modifier_states = []
for mod in mesh_obj.modifiers:
if mod.type == 'ARMATURE':
modifier_states.append({
'name': mod.name,
'object': mod.object,
'vertex_group': mod.vertex_group,
'show_viewport': mod.show_viewport
})
return modifier_states
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Object]) -> Tuple[bool, str]:
"""Apply current pose as rest pose for armature and update meshes"""
try:
logger.info(f"Starting pose application for {len(meshes)} meshes")
with ProgressTracker(context, len(meshes), "Applying Pose") as progress:
for mesh_obj in meshes:
if not mesh_obj.data:
continue
if mesh_obj.data.shape_keys and mesh_obj.data.shape_keys.key_blocks:
apply_armature_to_mesh_with_shapekeys(armature_obj, mesh_obj, context)
else:
apply_armature_to_mesh(armature_obj, mesh_obj)
progress.step(f"Processed {mesh_obj.name}")
bpy.ops.object.mode_set(mode='POSE') bpy.ops.object.mode_set(mode='POSE')
bpy.ops.pose.armature_apply(selected=False) bpy.ops.pose.armature_apply(selected=False)
bpy.ops.object.mode_set(mode='OBJECT') bpy.ops.object.mode_set(mode='OBJECT')
return True
return True, t("Operation.pose_applied")
except Exception as e:
logger.error(f"Error applying pose as rest: {str(e)}")
return False, str(e)
def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None: def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
"""Apply armature deformation to mesh"""
armature_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE') armature_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
armature_mod.object = armature_obj armature_mod.object = armature_obj
@@ -109,6 +261,7 @@ def apply_armature_to_mesh(armature_obj: Object, mesh_obj: Object) -> None:
bpy.ops.object.modifier_apply(modifier=armature_mod.name) bpy.ops.object.modifier_apply(modifier=armature_mod.name)
def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object, context: Context) -> None: def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object, context: Context) -> None:
"""Apply armature deformation to mesh with shape keys"""
old_active_index = mesh_obj.active_shape_key_index old_active_index = mesh_obj.active_shape_key_index
old_show_only = mesh_obj.show_only_shape_key old_show_only = mesh_obj.show_only_shape_key
mesh_obj.show_only_shape_key = True mesh_obj.show_only_shape_key = True
@@ -156,4 +309,4 @@ def apply_armature_to_mesh_with_shapekeys(armature_obj: Object, mesh_obj: Object
sk.mute = mute sk.mute = mute
mesh_obj.active_shape_key_index = old_active_index mesh_obj.active_shape_key_index = old_active_index
mesh_obj.show_only_shape_key = old_show_only mesh_obj.show_only_shape_key = old_show_only
+143 -95
View File
@@ -1,120 +1,168 @@
import bpy import bpy
import numpy as np import logging
from bpy.types import Operator, Context, Object from typing import Set, Dict, List, Tuple, Optional, Any
from typing import List from bpy.props import StringProperty
from bpy.types import Operator, Context, Object, Event, Modifier
from ..core.translations import t from ..core.translations import t
from ..core.common import ( from ..core.common import (
get_active_armature, get_active_armature,
get_all_meshes, get_all_meshes,
apply_pose_as_rest, apply_pose_as_rest,
apply_armature_to_mesh, validate_armature,
apply_armature_to_mesh_with_shapekeys, cache_vertex_positions,
validate_armature apply_vertex_positions,
validate_mesh_for_pose,
process_armature_modifiers,
ProgressTracker
) )
logger = logging.getLogger('avatar_toolkit.pose')
class BatchPoseOperationMixin:
"""Base class for batch pose operations"""
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature:
return False
valid, _ = validate_armature(armature)
return valid and context.mode == 'POSE'
def validate_meshes(self, meshes: List[Object]) -> List[Tuple[Object, str]]:
"""Validate meshes for pose operations"""
invalid_meshes = []
for mesh in meshes:
valid, message = validate_mesh_for_pose(mesh)
if not valid:
invalid_meshes.append((mesh, message))
return invalid_meshes
class AvatarToolkit_OT_StartPoseMode(Operator): class AvatarToolkit_OT_StartPoseMode(Operator):
bl_idname = 'avatar_toolkit.start_pose_mode' bl_idname = 'avatar_toolkit.start_pose_mode'
bl_label = t("Quick_Access.start_pose_mode.label") bl_label = t("QuickAccess.start_pose_mode.label")
bl_description = t("Quick_Access.start_pose_mode.desc") bl_description = t("QuickAccess.start_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
armature = get_active_armature(context) armature = get_active_armature(context)
if not armature or context.mode == "POSE": if not armature or context.mode == "POSE":
return False return False
is_valid, _ = validate_armature(armature) valid, _ = validate_armature(armature)
return is_valid return valid
def execute(self, context: Context) -> set[str]: def execute(self, context: Context) -> Set[str]:
armature = get_active_armature(context) try:
context.view_layer.objects.active = armature armature = get_active_armature(context)
bpy.ops.object.mode_set(mode='OBJECT') logger.info(f"Starting pose mode for armature: {armature.name}")
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True) bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.mode_set(mode='POSE') bpy.ops.object.select_all(action='DESELECT')
return {'FINISHED'}
context.view_layer.objects.active = armature
armature.select_set(True)
bpy.ops.object.mode_set(mode='POSE')
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to start pose mode: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.start", error=str(e)))
return {'CANCELLED'}
class AvatarToolkit_OT_StopPoseMode(Operator): class AvatarToolkit_OT_StopPoseMode(Operator):
bl_idname = 'avatar_toolkit.stop_pose_mode' bl_idname = 'avatar_toolkit.stop_pose_mode'
bl_label = t("Quick_Access.stop_pose_mode.label") bl_label = t("QuickAccess.stop_pose_mode.label")
bl_description = t("Quick_Access.stop_pose_mode.desc") bl_description = t("QuickAccess.stop_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'} bl_options = {'REGISTER', 'UNDO'}
@classmethod @classmethod
def poll(cls, context): def poll(cls, context: Context) -> bool:
return get_active_armature(context) and context.mode == "POSE" return get_active_armature(context) and context.mode == "POSE"
def execute(self, context: Context) -> set[str]:
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
bpy.ops.object.mode_set(mode='OBJECT')
return {'FINISHED'}
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator): def execute(self, context: Context) -> Set[str]:
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey' try:
bl_label = t("Quick_Access.apply_pose_as_shapekey.label") bpy.ops.pose.transforms_clear()
bl_description = t("Quick_Access.apply_pose_as_shapekey.desc") bpy.ops.pose.select_all(action="INVERT")
bl_options = {'REGISTER', 'UNDO'} bpy.ops.pose.transforms_clear()
bpy.ops.pose.select_all(action="INVERT")
@classmethod bpy.ops.object.mode_set(mode='OBJECT')
def poll(cls, context): return {'FINISHED'}
armature = get_active_armature(context) except Exception as e:
if not armature or context.mode != 'POSE': logger.error(f"Failed to stop pose mode: {str(e)}")
return False self.report({'ERROR'}, t("PoseMode.error.stop", error=str(e)))
is_valid, _ = validate_armature(armature) return {'CANCELLED'}
return is_valid
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
def execute(self, context): bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
armature_obj = get_active_armature(context) bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
mesh_objects = get_all_meshes(context) bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
bl_options = {'REGISTER', 'UNDO'}
for mesh_obj in mesh_objects:
if not mesh_obj.data: shapekey_name: StringProperty(
continue name=t("PoseMode.shapekey.name"),
description=t("PoseMode.shapekey.description"),
if not mesh_obj.data.shape_keys: default=t("PoseMode.shapekey.default")
mesh_obj.shape_key_add(name='Basis') )
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False) def invoke(self, context: Context, event: Event) -> Set[str]:
return context.window_manager.invoke_props_dialog(self)
depsgraph = context.evaluated_depsgraph_get()
eval_mesh = mesh_obj.evaluated_get(depsgraph) def execute(self, context: Context) -> Set[str]:
try:
for i, v in enumerate(eval_mesh.data.vertices): meshes = get_all_meshes(context)
new_shape.data[i].co = v.co.copy() invalid_meshes = self.validate_meshes(meshes)
bpy.ops.pose.select_all(action='SELECT') if invalid_meshes:
bpy.ops.pose.transforms_clear() message = "\n".join(f"{mesh.name}: {reason}" for mesh, reason in invalid_meshes)
bpy.ops.object.mode_set(mode='OBJECT') self.report({'WARNING'}, t("PoseMode.skipped_meshes", message=message))
self.report({'INFO'}, t('Tools.apply_pose_as_rest.success')) valid_meshes = [mesh for mesh in meshes if mesh not in [m for m, _ in invalid_meshes]]
return {'FINISHED'}
with ProgressTracker(context, len(valid_meshes), "Applying Pose as Shape Key") as progress:
class AvatarToolkit_OT_ApplyPoseAsRest(Operator): for mesh_obj in valid_meshes:
bl_idname = 'avatar_toolkit.apply_pose_as_rest' if not mesh_obj.data.shape_keys:
bl_label = t("Quick_Access.apply_pose_as_rest.label") mesh_obj.shape_key_add(name=t("PoseMode.basis"))
bl_description = t("Quick_Access.apply_pose_as_rest.desc")
bl_options = {'REGISTER', 'UNDO'} new_shape = mesh_obj.shape_key_add(name=self.shapekey_name, from_mix=False)
cached_positions = cache_vertex_positions(
@classmethod mesh_obj.evaluated_get(context.evaluated_depsgraph_get())
def poll(cls, context): )
armature = get_active_armature(context) apply_vertex_positions(new_shape.data, cached_positions)
if not armature or context.mode != "POSE": progress.step(f"Processed {mesh_obj.name}")
return False
is_valid, _ = validate_armature(armature) return {'FINISHED'}
return is_valid except Exception as e:
logger.error(f"Failed to apply pose as shape key: {str(e)}")
def execute(self, context): self.report({'ERROR'}, t("PoseMode.error.shapekey", error=str(e)))
if not apply_pose_as_rest( return {'CANCELLED'}
context=context,
armature_obj=get_active_armature(context), class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
meshes=get_all_meshes(context) bl_idname = 'avatar_toolkit.apply_pose_as_rest'
): bl_label = t("QuickAccess.apply_pose_as_rest.label")
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed")) bl_description = t("QuickAccess.apply_pose_as_rest.desc")
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context: Context) -> Set[str]:
try:
armature_obj = get_active_armature(context)
meshes = get_all_meshes(context)
invalid_meshes = self.validate_meshes(meshes)
if invalid_meshes:
message = "\n".join(f"{mesh.name}: {reason}" for mesh, reason in invalid_meshes)
self.report({'WARNING'}, t("PoseMode.skipped_meshes", message=message))
valid_meshes = [mesh for mesh in meshes if mesh not in [m for m, _ in invalid_meshes]]
with ProgressTracker(context, len(valid_meshes) + 2, "Applying Pose as Rest") as progress:
success, message = apply_pose_as_rest(context, armature_obj, valid_meshes)
if not success:
raise ValueError(message)
progress.step("Applied pose to armature")
logger.info("Successfully applied pose as rest")
return {'FINISHED'}
except Exception as e:
logger.error(f"Failed to apply pose as rest: {str(e)}")
self.report({'ERROR'}, t("PoseMode.error.rest_pose", error=str(e)))
return {'CANCELLED'} return {'CANCELLED'}
self.report({'INFO'}, t("Tools.apply_pose_as_rest.success"))
return {'FINISHED'}
+31 -12
View File
@@ -12,6 +12,7 @@
"Updater.UpdateToLatestButton.label": "Update to {name}", "Updater.UpdateToLatestButton.label": "Update to {name}",
"Updater.UpdateToSelectedButton.label": "Update", "Updater.UpdateToSelectedButton.label": "Update",
"Updater.currentVersion": "Current Version: {name}", "Updater.currentVersion": "Current Version: {name}",
"Updater.selectVersion": "Select Version",
"Updater.CheckForUpdateButton.desc": "Check for available updates", "Updater.CheckForUpdateButton.desc": "Check for available updates",
"UpdateToLatestButton.desc": "Update to the latest version", "UpdateToLatestButton.desc": "Update to the latest version",
"UpdateNotificationPopup.label": "Update Notification", "UpdateNotificationPopup.label": "Update Notification",
@@ -36,23 +37,41 @@
"QuickAccess.export": "Export", "QuickAccess.export": "Export",
"QuickAccess.export_fbx": "Export FBX", "QuickAccess.export_fbx": "Export FBX",
"QuickAccess.export_resonite": "Export to Resonite", "QuickAccess.export_resonite": "Export to Resonite",
"QuickAccess.start_pose_mode.label": "Start Pose Mode",
"QuickAccess.start_pose_mode.desc": "Enter pose mode for the selected armature",
"QuickAccess.stop_pose_mode.label": "Stop Pose Mode",
"QuickAccess.stop_pose_mode.desc": "Exit pose mode and clear transforms",
"QuickAccess.apply_pose_as_shapekey.label": "Apply Pose as Shape Key",
"QuickAccess.apply_pose_as_shapekey.desc": "Create a new shape key from current pose",
"QuickAccess.apply_pose_as_rest.label": "Apply Pose as Rest",
"QuickAccess.apply_pose_as_rest.desc": "Apply current pose as rest pose",
"QuickAccess.apply_armature_failed": "Failed to apply armature modifications",
"Quick_Access.start_pose_mode.label": "Start Pose Mode", "PoseMode.error.start": "Failed to start pose mode: {error}",
"Quick_Access.start_pose_mode.desc": "Enter pose mode for the selected armature", "PoseMode.error.stop": "Failed to stop pose mode: {error}",
"Quick_Access.stop_pose_mode.label": "Stop Pose Mode", "PoseMode.error.shapekey": "Failed to apply pose as shape key: {error}",
"Quick_Access.stop_pose_mode.desc": "Exit pose mode and clear transforms", "PoseMode.error.rest_pose": "Failed to apply pose as rest: {error}",
"Quick_Access.apply_pose_as_shapekey.label": "Apply Pose as Shape Key", "PoseMode.shapekey.name": "Shape Key Name",
"Quick_Access.apply_pose_as_shapekey.desc": "Create a new shape key from current pose", "PoseMode.shapekey.description": "Name for the new shape key",
"Quick_Access.apply_pose_as_rest.label": "Apply Pose as Rest", "PoseMode.shapekey.default": "Pose_Shapekey",
"Quick_Access.apply_pose_as_rest.desc": "Apply current pose as rest pose", "PoseMode.skipped_meshes": "Some meshes were skipped:\n{message}",
"Quick_Access.apply_armature_failed": "Failed to apply armature modifications", "PoseMode.basis": "Basis",
"Tools.apply_pose_as_rest.success": "Successfully applied pose as rest position",
"Armature.validation.no_armature": "No armature selected", "Armature.validation.no_armature": "No armature selected",
"Armature.validation.not_armature": "Selected object is not an armature", "Armature.validation.not_armature": "Selected object is not an armature",
"Armature.validation.no_bones": "Armature has no bones", "Armature.validation.no_bones": "Armature has no bones",
"Armature.validation.missing_bone": "Missing essential bone: {bone}", "Armature.validation.basic_check_failed": "Basic armature validation failed",
"Armature.validation.missing_bones": "Missing essential bones: {bones}",
"Armature.validation.invalid_hierarchy": "Invalid bone hierarchy between {parent} and {child}",
"Armature.validation.asymmetric_bones": "Missing symmetric bones for {bone}",
"Armature.validation.asymmetric_hand_wrist": "Missing symmetric bones for hands/wrists",
"Mesh.validation.no_data": "No mesh data",
"Mesh.validation.no_vertex_groups": "No vertex groups found",
"Mesh.validation.no_armature_modifier": "No armature modifier",
"Mesh.validation.valid": "Valid mesh for pose operations",
"Operation.pose_applied": "Pose applied successfully",
"Scene.avatar_toolkit_updater_version_list.name": "Version List", "Scene.avatar_toolkit_updater_version_list.name": "Version List",
"Scene.avatar_toolkit_updater_version_list.description": "List of available versions" "Scene.avatar_toolkit_updater_version_list.description": "List of available versions"
+4 -4
View File
@@ -87,9 +87,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
# Armature Validation # Armature Validation
active_armature = get_active_armature(context) active_armature = get_active_armature(context)
if active_armature: if active_armature:
is_valid: bool is_valid, messages = validate_armature(active_armature)
message: str
is_valid, message = validate_armature(active_armature)
if is_valid: if is_valid:
info_box: UILayout = col.box() info_box: UILayout = col.box()
@@ -103,7 +101,9 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT') info_box.label(text=t("QuickAccess.pose_bones_available"), icon='POSE_HLT')
else: else:
col.separator(factor=0.5) col.separator(factor=0.5)
col.label(text=message, icon='ERROR') # Display each validation message
for message in messages:
col.label(text=message, icon='ERROR')
# Pose Mode Controls # Pose Mode Controls
pose_box: UILayout = layout.box() pose_box: UILayout = layout.box()