Files
Avatar-Toolkit/functions/custom_tools/mesh_attachment.py
T
989onan 6d9f751a16 Housekeeping (bug fixes)
NEW FEATURES:
- added apply shapekey to basis from Cats
  - now that pesky thing I keep going back to cats for is in Avatar Toolkit.

BUG FIXES:
- now we push armature santizers into functions where they are needed
  - this prevents the methods from mirroring changes while working, causing them to blow up when mirror mode is on
  - more changes to come for armature setting santitizers
- fixed error reporting
  - now methods when catching errors will return full error tracebacks
  - this will help make debugging and finding user issues easier.
2025-07-10 18:44:42 -04:00

143 lines
5.9 KiB
Python

import bpy
from bpy.types import Operator, Context, Object, ArmatureModifier, VertexGroup
from mathutils import Vector
from typing import Set, Optional, List, Any
import traceback
from ...core.logging_setup import logger
from ...core.translations import t
from ...core.common import (
get_active_armature,
get_all_meshes,
ProgressTracker,
calculate_bone_orientation,
add_armature_modifier,
store_breaking_settings_armature,
restore_breaking_settings_armature,
)
from ...core.armature_validation import validate_armature
class AvatarToolkit_OT_AttachMesh(Operator):
"""Operator to attach a mesh to an armature bone with automatic weight setup"""
bl_idname: str = "avatar_toolkit.attach_mesh"
bl_label: str = t("AttachMesh.label")
bl_description: str = t("AttachMesh.desc")
bl_options: Set[str] = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: Context) -> bool:
"""Check if operator can be executed"""
armature: Optional[Object] = get_active_armature(context)
if not armature:
return False
valid, _, _ = validate_armature(armature)
return valid
def execute(self, context: Context) -> Set[str]:
try:
logger.info("Starting mesh attachment process")
mesh_name: str = context.scene.avatar_toolkit.attach_mesh
armature: Object = get_active_armature(context)
attach_bone_name: str = context.scene.avatar_toolkit.attach_bone
mesh: Optional[Object] = bpy.data.objects.get(mesh_name)
with ProgressTracker(context, 10, "Attaching Mesh") as progress:
# Validation steps
is_valid: bool
error_msg: str
is_valid, error_msg = validate_mesh_transforms(mesh)
if not is_valid:
raise ValueError(error_msg)
progress.step(t("AttachMesh.validate_transforms"))
is_valid, error_msg = validate_mesh_name(armature, mesh_name)
if not is_valid:
raise ValueError(error_msg)
progress.step(t("AttachMesh.validate_name"))
# Parent mesh to armature
mesh.parent = armature
mesh.parent_type = 'OBJECT'
progress.step(t("AttachMesh.parent_mesh"))
# Setup vertex groups
if mesh.vertex_groups:
for vg in mesh.vertex_groups:
mesh.vertex_groups.remove(vg)
bpy.ops.object.select_all(action='DESELECT')
mesh.select_set(True)
context.view_layer.objects.active = mesh
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='SELECT')
vg: VertexGroup = mesh.vertex_groups.new(name=mesh_name)
bpy.ops.object.vertex_group_assign()
bpy.ops.object.mode_set(mode='OBJECT')
progress.step(t("AttachMesh.setup_weights"))
# Create and setup bone
bpy.ops.object.select_all(action='DESELECT')
armature.select_set(True)
context.view_layer.objects.active = armature
bpy.ops.object.mode_set(mode='EDIT')
attach_to_bone = armature.data.edit_bones.get(attach_bone_name)
if not attach_to_bone:
raise ValueError(t("AttachMesh.error.bone_not_found", bone=attach_bone_name))
data_breaking = store_breaking_settings_armature(armature)
mesh_bone = armature.data.edit_bones.new(mesh_name)
mesh_bone.parent = attach_to_bone
progress.step(t("AttachMesh.create_bone"))
# Calculate bone placement
verts_in_group: List[Any] = [v for v in mesh.data.vertices
for g in v.groups if g.group == vg.index]
dimensions: Vector
roll_angle: float
dimensions, roll_angle = calculate_bone_orientation(mesh, verts_in_group)
# Set bone position and orientation
center: Vector = Vector((0, 0, 0))
for v in verts_in_group:
center += mesh.data.vertices[v.index].co
center /= len(verts_in_group)
mesh_bone.head = center
mesh_bone.tail = center + Vector((0, 0, max(0.1, dimensions.z)))
mesh_bone.roll = roll_angle
restore_breaking_settings_armature(armature, data_breaking)
progress.step(t("AttachMesh.position_bone"))
bpy.ops.object.mode_set(mode='OBJECT')
add_armature_modifier(mesh, armature)
progress.step(t("AttachMesh.add_modifier"))
logger.info(f"Successfully attached mesh {mesh_name} to bone {attach_bone_name}")
self.report({'INFO'}, t("AttachMesh.success"))
return {'FINISHED'}
except Exception:
logger.error(f"Failed to attach mesh: {traceback.format_exc()}")
self.report({'ERROR'}, traceback.format_exc())
return {'CANCELLED'}
def validate_mesh_transforms(mesh: Optional[Object]) -> tuple[bool, str]:
"""Validate mesh transforms are suitable for attaching"""
if not mesh:
return False, "Mesh not found"
# Check for non-uniform scale
scale: Vector = mesh.scale
if abs(scale[0] - scale[1]) > 0.001 or abs(scale[1] - scale[2]) > 0.001:
return False, "Mesh has non-uniform scale. Please apply scale (Ctrl+A)"
return True, ""
def validate_mesh_name(armature: Object, mesh_name: str) -> tuple[bool, str]:
"""Validate mesh name doesn't conflict with existing bones"""
if mesh_name in armature.data.bones:
return False, f"Bone named '{mesh_name}' already exists in armature"
return True, ""