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:
+143
-95
@@ -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
|
||||
|
||||
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'}
|
||||
valid, _ = validate_armature(armature)
|
||||
return valid
|
||||
|
||||
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'}
|
||||
|
||||
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'}
|
||||
|
||||
self.report({'INFO'}, t("Tools.apply_pose_as_rest.success"))
|
||||
return {'FINISHED'}
|
||||
|
||||
Reference in New Issue
Block a user