Files
Avatar-Toolkit/functions/pose_mode.py
T
Yusarina 9961223548 Setting Panel Added, Debuging Added.
Added the Armature Validation modes now, we have Stritct, Basic and None, it will give a warning to the user in the panel if there have it set to basic or none.
Settings panel added, langauge change has been added back. Did some work on it to slightl improve the system.
Added dubug area, basically everything but autoload will use logging now, you be able to turn it on/off in debug settings.
Did other bits and bobs.
2024-12-04 14:58:34 +00:00

167 lines
6.8 KiB
Python

import bpy
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.logging_setup import logger
from ..core.translations import t
from ..core.common import (
get_active_armature,
get_all_meshes,
apply_pose_as_rest,
validate_armature,
cache_vertex_positions,
apply_vertex_positions,
validate_mesh_for_pose,
process_armature_modifiers,
ProgressTracker
)
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("QuickAccess.start_pose_mode.label")
bl_description = t("QuickAccess.start_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
armature = get_active_armature(context)
if not armature or context.mode == "POSE":
return False
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("QuickAccess.stop_pose_mode.label")
bl_description = t("QuickAccess.stop_pose_mode.desc")
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
return get_active_armature(context) and context.mode == "POSE"
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'}