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:
+189
-36
@@ -1,10 +1,48 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy.types import Context, Object
|
||||
from typing import Optional, Tuple, List, Set
|
||||
import logging
|
||||
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.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]:
|
||||
"""Get the currently selected armature from Avatar Toolkit properties"""
|
||||
armature_name = context.scene.avatar_toolkit.active_armature
|
||||
@@ -25,26 +63,94 @@ def get_armature_list(self=None, context: bpy.types.Context = None) -> List[Tupl
|
||||
return [('NONE', t("Armature.validation.no_armature"), '')]
|
||||
return armatures
|
||||
|
||||
def validate_armature(armature: bpy.types.Object) -> Tuple[bool, str]:
|
||||
"""
|
||||
Validate if the selected object is a proper armature and has required bones
|
||||
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")
|
||||
def validate_armature(armature: bpy.types.Object, validation_level: str = 'standard') -> Tuple[bool, List[str]]:
|
||||
"""Enhanced armature validation with multiple checks and validation levels"""
|
||||
messages = []
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
return True, t("QuickAccess.valid_armature")
|
||||
|
||||
def auto_select_single_armature(context: bpy.types.Context) -> None:
|
||||
"""Automatically select armature if only one exists in scene"""
|
||||
@@ -69,33 +175,79 @@ def get_armature_stats(armature: bpy.types.Object) -> dict:
|
||||
}
|
||||
|
||||
def get_all_meshes(context: Context) -> List[Object]:
|
||||
"""Get all mesh objects parented to the active armature"""
|
||||
armature = get_active_armature(context)
|
||||
if armature:
|
||||
return [obj for obj in bpy.data.objects if obj.type == 'MESH' and obj.parent == armature]
|
||||
return []
|
||||
|
||||
def apply_pose_as_rest(context: Context, armature_obj: Object, meshes: List[Object]) -> bool:
|
||||
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:
|
||||
if len(mesh_obj.data.shape_keys.key_blocks) == 1:
|
||||
basis = mesh_obj.data.shape_keys.key_blocks[0]
|
||||
basis_name = basis.name
|
||||
mesh_obj.shape_key_remove(basis)
|
||||
apply_armature_to_mesh(armature_obj, mesh_obj)
|
||||
mesh_obj.shape_key_add(name=basis_name)
|
||||
else:
|
||||
apply_armature_to_mesh_with_shapekeys(armature_obj, mesh_obj, context)
|
||||
else:
|
||||
apply_armature_to_mesh(armature_obj, mesh_obj)
|
||||
def validate_mesh_for_pose(mesh_obj: Object) -> Tuple[bool, str]:
|
||||
"""Validate mesh object for pose operations"""
|
||||
if not mesh_obj.data:
|
||||
return False, t("Mesh.validation.no_data")
|
||||
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
bpy.ops.pose.armature_apply(selected=False)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
return True
|
||||
if not mesh_obj.vertex_groups:
|
||||
return False, t("Mesh.validation.no_vertex_groups")
|
||||
|
||||
armature_mods = [mod for mod in mesh_obj.modifiers if mod.type == 'ARMATURE']
|
||||
if not armature_mods:
|
||||
return False, t("Mesh.validation.no_armature_modifier")
|
||||
|
||||
return True, t("Mesh.validation.valid")
|
||||
|
||||
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.pose.armature_apply(selected=False)
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
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:
|
||||
"""Apply armature deformation to mesh"""
|
||||
armature_mod = mesh_obj.modifiers.new('PoseToRest', 'ARMATURE')
|
||||
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)
|
||||
|
||||
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_show_only = mesh_obj.show_only_shape_key
|
||||
mesh_obj.show_only_shape_key = True
|
||||
|
||||
+141
-93
@@ -1,120 +1,168 @@
|
||||
import bpy
|
||||
import numpy as np
|
||||
from bpy.types import Operator, Context, Object
|
||||
from typing import List
|
||||
import logging
|
||||
from typing import Set, Dict, List, Tuple, Optional, Any
|
||||
from bpy.props import StringProperty
|
||||
from bpy.types import Operator, Context, Object, Event, Modifier
|
||||
from ..core.translations import t
|
||||
from ..core.common import (
|
||||
get_active_armature,
|
||||
get_all_meshes,
|
||||
apply_pose_as_rest,
|
||||
apply_armature_to_mesh,
|
||||
apply_armature_to_mesh_with_shapekeys,
|
||||
validate_armature
|
||||
validate_armature,
|
||||
cache_vertex_positions,
|
||||
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):
|
||||
bl_idname = 'avatar_toolkit.start_pose_mode'
|
||||
bl_label = t("Quick_Access.start_pose_mode.label")
|
||||
bl_description = t("Quick_Access.start_pose_mode.desc")
|
||||
bl_label = t("QuickAccess.start_pose_mode.label")
|
||||
bl_description = t("QuickAccess.start_pose_mode.desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
armature = get_active_armature(context)
|
||||
if not armature or context.mode == "POSE":
|
||||
return False
|
||||
is_valid, _ = validate_armature(armature)
|
||||
return is_valid
|
||||
valid, _ = validate_armature(armature)
|
||||
return valid
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
armature = get_active_armature(context)
|
||||
context.view_layer.objects.active = armature
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
armature.select_set(True)
|
||||
bpy.ops.object.mode_set(mode='POSE')
|
||||
return {'FINISHED'}
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
armature = get_active_armature(context)
|
||||
logger.info(f"Starting pose mode for armature: {armature.name}")
|
||||
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
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):
|
||||
bl_idname = 'avatar_toolkit.stop_pose_mode'
|
||||
bl_label = t("Quick_Access.stop_pose_mode.label")
|
||||
bl_description = t("Quick_Access.stop_pose_mode.desc")
|
||||
bl_label = t("QuickAccess.stop_pose_mode.label")
|
||||
bl_description = t("QuickAccess.stop_pose_mode.desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
def poll(cls, context: Context) -> bool:
|
||||
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):
|
||||
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
||||
bl_label = t("Quick_Access.apply_pose_as_shapekey.label")
|
||||
bl_description = t("Quick_Access.apply_pose_as_shapekey.desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
if not armature or context.mode != 'POSE':
|
||||
return False
|
||||
is_valid, _ = validate_armature(armature)
|
||||
return is_valid
|
||||
|
||||
def execute(self, context):
|
||||
armature_obj = get_active_armature(context)
|
||||
mesh_objects = get_all_meshes(context)
|
||||
|
||||
for mesh_obj in mesh_objects:
|
||||
if not mesh_obj.data:
|
||||
continue
|
||||
|
||||
if not mesh_obj.data.shape_keys:
|
||||
mesh_obj.shape_key_add(name='Basis')
|
||||
|
||||
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False)
|
||||
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
eval_mesh = mesh_obj.evaluated_get(depsgraph)
|
||||
|
||||
for i, v in enumerate(eval_mesh.data.vertices):
|
||||
new_shape.data[i].co = v.co.copy()
|
||||
|
||||
bpy.ops.pose.select_all(action='SELECT')
|
||||
bpy.ops.pose.transforms_clear()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
|
||||
self.report({'INFO'}, t('Tools.apply_pose_as_rest.success'))
|
||||
return {'FINISHED'}
|
||||
|
||||
class AvatarToolkit_OT_ApplyPoseAsRest(Operator):
|
||||
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
||||
bl_label = t("Quick_Access.apply_pose_as_rest.label")
|
||||
bl_description = t("Quick_Access.apply_pose_as_rest.desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
armature = get_active_armature(context)
|
||||
if not armature or context.mode != "POSE":
|
||||
return False
|
||||
is_valid, _ = validate_armature(armature)
|
||||
return is_valid
|
||||
|
||||
def execute(self, context):
|
||||
if not apply_pose_as_rest(
|
||||
context=context,
|
||||
armature_obj=get_active_armature(context),
|
||||
meshes=get_all_meshes(context)
|
||||
):
|
||||
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
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'}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to stop pose mode: {str(e)}")
|
||||
self.report({'ERROR'}, t("PoseMode.error.stop", error=str(e)))
|
||||
return {'CANCELLED'}
|
||||
|
||||
self.report({'INFO'}, t("Tools.apply_pose_as_rest.success"))
|
||||
return {'FINISHED'}
|
||||
class AvatarToolkit_OT_ApplyPoseAsRest(Operator, BatchPoseOperationMixin):
|
||||
bl_idname = 'avatar_toolkit.apply_pose_as_shapekey'
|
||||
bl_label = t("QuickAccess.apply_pose_as_shapekey.label")
|
||||
bl_description = t("QuickAccess.apply_pose_as_shapekey.desc")
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
shapekey_name: StringProperty(
|
||||
name=t("PoseMode.shapekey.name"),
|
||||
description=t("PoseMode.shapekey.description"),
|
||||
default=t("PoseMode.shapekey.default")
|
||||
)
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> Set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self)
|
||||
|
||||
def execute(self, context: Context) -> Set[str]:
|
||||
try:
|
||||
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), "Applying Pose as Shape Key") as progress:
|
||||
for mesh_obj in valid_meshes:
|
||||
if not mesh_obj.data.shape_keys:
|
||||
mesh_obj.shape_key_add(name=t("PoseMode.basis"))
|
||||
|
||||
new_shape = mesh_obj.shape_key_add(name=self.shapekey_name, from_mix=False)
|
||||
cached_positions = cache_vertex_positions(
|
||||
mesh_obj.evaluated_get(context.evaluated_depsgraph_get())
|
||||
)
|
||||
apply_vertex_positions(new_shape.data, cached_positions)
|
||||
progress.step(f"Processed {mesh_obj.name}")
|
||||
|
||||
return {'FINISHED'}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply pose as shape key: {str(e)}")
|
||||
self.report({'ERROR'}, t("PoseMode.error.shapekey", error=str(e)))
|
||||
return {'CANCELLED'}
|
||||
|
||||
class AvatarToolkit_OT_ApplyPoseAsShapekey(Operator, BatchPoseOperationMixin):
|
||||
bl_idname = 'avatar_toolkit.apply_pose_as_rest'
|
||||
bl_label = t("QuickAccess.apply_pose_as_rest.label")
|
||||
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'}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"Updater.UpdateToLatestButton.label": "Update to {name}",
|
||||
"Updater.UpdateToSelectedButton.label": "Update",
|
||||
"Updater.currentVersion": "Current Version: {name}",
|
||||
"Updater.selectVersion": "Select Version",
|
||||
"Updater.CheckForUpdateButton.desc": "Check for available updates",
|
||||
"UpdateToLatestButton.desc": "Update to the latest version",
|
||||
"UpdateNotificationPopup.label": "Update Notification",
|
||||
@@ -36,23 +37,41 @@
|
||||
"QuickAccess.export": "Export",
|
||||
"QuickAccess.export_fbx": "Export FBX",
|
||||
"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",
|
||||
"Quick_Access.start_pose_mode.desc": "Enter pose mode for the selected armature",
|
||||
"Quick_Access.stop_pose_mode.label": "Stop Pose Mode",
|
||||
"Quick_Access.stop_pose_mode.desc": "Exit pose mode and clear transforms",
|
||||
"Quick_Access.apply_pose_as_shapekey.label": "Apply Pose as Shape Key",
|
||||
"Quick_Access.apply_pose_as_shapekey.desc": "Create a new shape key from current pose",
|
||||
"Quick_Access.apply_pose_as_rest.label": "Apply Pose as Rest",
|
||||
"Quick_Access.apply_pose_as_rest.desc": "Apply current pose as rest pose",
|
||||
"Quick_Access.apply_armature_failed": "Failed to apply armature modifications",
|
||||
|
||||
"Tools.apply_pose_as_rest.success": "Successfully applied pose as rest position",
|
||||
"PoseMode.error.start": "Failed to start pose mode: {error}",
|
||||
"PoseMode.error.stop": "Failed to stop pose mode: {error}",
|
||||
"PoseMode.error.shapekey": "Failed to apply pose as shape key: {error}",
|
||||
"PoseMode.error.rest_pose": "Failed to apply pose as rest: {error}",
|
||||
"PoseMode.shapekey.name": "Shape Key Name",
|
||||
"PoseMode.shapekey.description": "Name for the new shape key",
|
||||
"PoseMode.shapekey.default": "Pose_Shapekey",
|
||||
"PoseMode.skipped_meshes": "Some meshes were skipped:\n{message}",
|
||||
"PoseMode.basis": "Basis",
|
||||
|
||||
"Armature.validation.no_armature": "No armature selected",
|
||||
"Armature.validation.not_armature": "Selected object is not an armature",
|
||||
"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.description": "List of available versions"
|
||||
|
||||
@@ -87,9 +87,7 @@ class AvatarToolKit_PT_QuickAccessPanel(Panel):
|
||||
# Armature Validation
|
||||
active_armature = get_active_armature(context)
|
||||
if active_armature:
|
||||
is_valid: bool
|
||||
message: str
|
||||
is_valid, message = validate_armature(active_armature)
|
||||
is_valid, messages = validate_armature(active_armature)
|
||||
|
||||
if is_valid:
|
||||
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')
|
||||
else:
|
||||
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_box: UILayout = layout.box()
|
||||
|
||||
Reference in New Issue
Block a user