7e584e3648
This fixes is to get everything working on the new auto load and properties system. Also some other small fixes.
454 lines
18 KiB
Python
454 lines
18 KiB
Python
import bpy
|
|
from bpy.types import Context, Mesh, Panel, Operator, Armature, EditBone
|
|
from ..core.translations import t
|
|
from ..core.common import get_selected_armature, get_all_meshes
|
|
from ..core import common
|
|
from ..core.dictionaries import bone_names
|
|
from mathutils import Matrix
|
|
|
|
|
|
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_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return get_selected_armature(context) != None and context.mode != "POSE"
|
|
|
|
def execute(self, context: Context) -> set[str]:
|
|
|
|
#give an active object so the next line doesn't throw an error.
|
|
context.view_layer.objects.active = get_selected_armature(context)
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
#deselect everything and select just our armature, then go into pose on just our selected armature. - @989onan
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
context.view_layer.objects.active = get_selected_armature(context)
|
|
context.view_layer.objects.active.select_set(True)
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
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_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return get_selected_armature(context) != None and context.mode == "POSE"
|
|
|
|
def execute(self, context: Context) -> set[str]:
|
|
#this is done so that transforms are cleared but user selection is respected. - @989onan
|
|
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 = common.get_selected_armature(context)
|
|
return armature and context.mode == 'POSE'
|
|
|
|
def execute(self, context):
|
|
armature_obj = common.get_selected_armature(context)
|
|
mesh_objects = common.get_all_meshes(context)
|
|
|
|
for mesh_obj in mesh_objects:
|
|
if not mesh_obj.data:
|
|
continue
|
|
|
|
# Ensure basis exists
|
|
if not mesh_obj.data.shape_keys:
|
|
mesh_obj.shape_key_add(name='Basis')
|
|
|
|
# Store current pose as new shapekey
|
|
new_shape = mesh_obj.shape_key_add(name='Pose_Shapekey', from_mix=False)
|
|
|
|
# Evaluate mesh in current pose
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
eval_mesh = mesh_obj.evaluated_get(depsgraph)
|
|
|
|
# Apply evaluated vertices to new shapekey
|
|
for i, v in enumerate(eval_mesh.data.vertices):
|
|
new_shape.data[i].co = v.co.copy()
|
|
|
|
# Reset pose
|
|
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):
|
|
return get_selected_armature(context) != None and context.mode == "POSE"
|
|
|
|
def execute(self, context: Context):
|
|
if not common.apply_pose_as_rest(armature_obj=get_selected_armature(context),
|
|
meshes=get_all_meshes(context),
|
|
context=context):
|
|
self.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
|
|
return {'CANCELLED'}
|
|
return {'FINISHED'}
|
|
|
|
|
|
class AvatarToolkit_OT_RemoveZeroWeightBones(Operator):
|
|
bl_idname = "avatar_toolkit.remove_zero_weight_bones"
|
|
bl_label = t("Tools.remove_zero_weight_bones.label")
|
|
bl_description = t("Tools.remove_zero_weight_bones.desc")
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
threshold: bpy.props.FloatProperty(
|
|
default=0.01,
|
|
name=t("Tools.remove_zero_weight_bones.threshold.label"),
|
|
description=t("Tools.remove_zero_weight_bones.threshold.desc"),
|
|
min=0.0000001,
|
|
max=0.9999999)
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
return common.get_selected_armature(context) is not None
|
|
|
|
def execute(self, context: Context) -> set[str]:
|
|
armature = common.get_selected_armature(context)
|
|
if not common.is_valid_armature(armature):
|
|
self.report({'ERROR'}, t("Tools.apply_transforms.invalid_armature"))
|
|
return {'CANCELLED'}
|
|
|
|
weighted_bones: list[str] = []
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
# Store initial transforms
|
|
initial_transforms = {}
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
for bone in armature.data.edit_bones:
|
|
initial_transforms[bone.name] = {
|
|
'head': bone.head.copy(),
|
|
'tail': bone.tail.copy(),
|
|
'roll': bone.roll,
|
|
'matrix': bone.matrix.copy(),
|
|
'parent': bone.parent.name if bone.parent else None
|
|
}
|
|
# Add end bones to transforms
|
|
if bone.name.endswith('_end'):
|
|
initial_transforms[bone.name] = {
|
|
'head': bone.head.copy(),
|
|
'tail': bone.tail.copy(),
|
|
'roll': bone.roll,
|
|
'matrix': bone.matrix.copy(),
|
|
'parent': bone.parent.name if bone.parent else None
|
|
}
|
|
|
|
# Get weighted bones
|
|
armature.select_set(True)
|
|
context.view_layer.objects.active = armature
|
|
|
|
meshes = common.get_all_meshes(context)
|
|
for mesh in meshes:
|
|
mesh_data: Mesh = mesh.data
|
|
for vertex in mesh_data.vertices:
|
|
for group in vertex.groups:
|
|
if group.weight > self.threshold:
|
|
weighted_bones.append(mesh.vertex_groups[group.group].name)
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
amature_data: Armature = armature.data
|
|
unweighted_bones: list[str] = []
|
|
|
|
# Identify unweighted bones
|
|
for bone in amature_data.edit_bones:
|
|
if bone.name not in weighted_bones:
|
|
unweighted_bones.append(bone.name)
|
|
|
|
# Process bone removal while preserving positions
|
|
for bone_name in unweighted_bones:
|
|
bone = amature_data.edit_bones[bone_name]
|
|
|
|
# Store children data
|
|
children = bone.children
|
|
children_data = {}
|
|
for child in children:
|
|
children_data[child.name] = initial_transforms[child.name]
|
|
|
|
# Reparent children
|
|
for child in children:
|
|
child.use_connect = False
|
|
if bone.parent:
|
|
child.parent = bone.parent
|
|
|
|
# Remove bone
|
|
amature_data.edit_bones.remove(bone)
|
|
|
|
# Restore children positions
|
|
for child_name, data in children_data.items():
|
|
if child_name in amature_data.edit_bones:
|
|
child = amature_data.edit_bones[child_name]
|
|
child.head = data['head']
|
|
child.tail = data['tail']
|
|
child.roll = data['roll']
|
|
child.matrix = data['matrix']
|
|
|
|
# Final position verification
|
|
for bone_name, transform in initial_transforms.items():
|
|
if bone_name in amature_data.edit_bones:
|
|
bone = amature_data.edit_bones[bone_name]
|
|
bone.matrix = transform['matrix']
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
self.report({'INFO'}, t("Tools.remove_zero_weight_bones.success"))
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
class AvatarToolkit_OT_MergeBonesToActive(Operator):
|
|
bl_idname = "avatar_toolkit.merge_bones_to_active"
|
|
bl_label = t("Tools.merge_bones_to_active.label")
|
|
bl_description = t("Tools.merge_bones_to_active.desc")
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
delete_old: bpy.props.BoolProperty(name=t("Tools.merge_bones_to_active.delete_old.label"), description=t("Tools.merge_bones_to_active.delete_old.desc"), default=False)
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
if common.get_selected_armature(context) is not None:
|
|
if common.get_selected_armature(context) == context.view_layer.objects.active:
|
|
if context.mode == "POSE":
|
|
return len(context.selected_pose_bones) > 1
|
|
elif context.mode == "EDIT_ARMATURE":
|
|
return len(context.selected_bones) > 1
|
|
return False
|
|
|
|
def execute(cls, context: Context) -> set[str]:
|
|
|
|
prev_mode: str = "EDIT"
|
|
if context.mode == "POSE":
|
|
prev_mode = "POSE"
|
|
|
|
#get active bone and a list of all other selected bones
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
target_bone: str = context.active_bone.name
|
|
|
|
armature_data: Armature = context.view_layer.objects.active.data
|
|
|
|
|
|
bones: list[str] = [i.name for i in context.selected_bones]
|
|
bones.remove(target_bone)
|
|
|
|
for obj in common.get_all_meshes(context):
|
|
for bone in bones:
|
|
bone_name: str = armature_data.edit_bones[bone].name
|
|
common.transfer_vertex_weights(context=context,obj=obj,source_group=bone_name,target_group=armature_data.edit_bones[target_bone].name)
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
for bone in bones:
|
|
if cls.delete_old:
|
|
for bone_child in armature_data.edit_bones[bone].children:
|
|
bone_child.parent = armature_data.edit_bones[bone].parent
|
|
armature_data.edit_bones.remove(armature_data.edit_bones[bone])
|
|
|
|
bpy.ops.object.mode_set(mode=prev_mode)
|
|
return {'FINISHED'}
|
|
|
|
|
|
class AvatarToolkit_OT_MergeBonesToParents(Operator):
|
|
bl_idname = "avatar_toolkit.merge_bones_to_parents"
|
|
bl_label = t("Tools.merge_bones_to_parents.label")
|
|
bl_description = t("Tools.merge_bones_to_parents.desc")
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
delete_old: bpy.props.BoolProperty(
|
|
name=t("Tools.merge_bones_to_parents.delete_old.label"),
|
|
description=t("Tools.merge_bones_to_parents.delete_old.desc"),
|
|
default=False
|
|
)
|
|
|
|
@classmethod
|
|
def poll(cls, context: Context) -> bool:
|
|
armature = common.get_selected_armature(context)
|
|
if armature and armature == context.view_layer.objects.active:
|
|
if context.mode == "POSE":
|
|
return len(context.selected_pose_bones) > 0
|
|
elif context.mode == "EDIT_ARMATURE":
|
|
return len(context.selected_editable_bones) > 0
|
|
return False
|
|
|
|
def execute(self, context: Context) -> set[str]:
|
|
prev_mode = context.mode
|
|
|
|
# Map 'EDIT_ARMATURE' to 'EDIT' for bpy.ops.object.mode_set
|
|
if prev_mode == 'EDIT_ARMATURE':
|
|
prev_mode = 'EDIT'
|
|
|
|
# Switch to Edit Mode
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
armature_data: Armature = context.view_layer.objects.active.data
|
|
|
|
# Get selected bones in Edit Mode
|
|
selected_bones = context.selected_editable_bones
|
|
selected_bone_names = [bone.name for bone in selected_bones]
|
|
|
|
if not selected_bone_names:
|
|
self.report({'ERROR'}, t("No bones selected"))
|
|
return {'CANCELLED'}
|
|
|
|
for obj in common.get_all_meshes(context):
|
|
for bone_name in selected_bone_names:
|
|
bone = armature_data.edit_bones.get(bone_name)
|
|
if bone and bone.parent:
|
|
# Transfer weights from bone to its parent
|
|
common.transfer_vertex_weights(
|
|
context=context,
|
|
obj=obj,
|
|
source_group=bone_name,
|
|
target_group=bone.parent.name
|
|
)
|
|
# Ensure we're in Edit Mode after transfer
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
else:
|
|
self.report({'WARNING'}, f"Bone '{bone_name}' has no parent or not found; skipping")
|
|
|
|
# Optionally delete old bones
|
|
if self.delete_old:
|
|
for bone_name in selected_bone_names:
|
|
bone = armature_data.edit_bones.get(bone_name)
|
|
if bone:
|
|
# Reassign children to the parent of the bone being deleted
|
|
for child in bone.children:
|
|
child.parent = bone.parent
|
|
# Remove the bone
|
|
armature_data.edit_bones.remove(bone)
|
|
else:
|
|
self.report({'WARNING'}, f"Bone '{bone_name}' not found in armature; cannot delete")
|
|
|
|
# Return to previous mode
|
|
bpy.ops.object.mode_set(mode=prev_mode)
|
|
return {'FINISHED'}
|
|
|
|
|
|
class AvatarToolkit_OT_MergeArmatures(Operator):
|
|
bl_idname = "avatar_toolkit.merge_armatures"
|
|
bl_label = t("MergeArmature.merge_armatures.label")
|
|
bl_description = t("MergeArmature.merge_armatures.desc").format(selected_armature_label=t("MergeArmatures.selected_armature.label"))
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return (common.get_selected_armature(context) is not None) and (common.get_merge_armature_source(context) is not None)
|
|
|
|
def make_active(self, obj: bpy.types.Object, context: Context):
|
|
context.view_layer.objects.active = obj
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
context.view_layer.objects.active = obj
|
|
obj.select_set(True)
|
|
|
|
def execute(cls, context: Context) -> set[str]:
|
|
source_armature: bpy.types.Object = bpy.data.objects[context.scene.merge_armature_source]
|
|
source_armature_data: Armature = source_armature.data
|
|
target_armature: bpy.types.Object = common.get_selected_armature(context)
|
|
target_armature_data: Armature = target_armature.data
|
|
parent_dictionary: dict[str, list[str]] = {}
|
|
|
|
cls.make_active(obj=source_armature, context=context)
|
|
|
|
|
|
|
|
if context.scene.merge_armature_apply_transforms:
|
|
target_armature.select_set(True)
|
|
for obj in target_armature.children:
|
|
obj.select_set(True)
|
|
for obj in source_armature.children:
|
|
obj.select_set(True)
|
|
bpy.ops.object.transform_apply()
|
|
|
|
|
|
if context.scene.merge_armature_align_bones:
|
|
if not context.scene.merge_armature_apply_transforms:
|
|
source_armature.matrix_world = target_armature.matrix_world
|
|
|
|
def children_bone_recursive(parent_bone) -> list[bpy.types.PoseBone]:
|
|
child_bones = []
|
|
child_bones.append(parent_bone)
|
|
for child in parent_bone.children:
|
|
child_bones.extend(children_bone_recursive(child))
|
|
return child_bones
|
|
bpy.ops.object.mode_set(mode='POSE')
|
|
source_armature_bone_names = [j.name for j in children_bone_recursive(
|
|
source_armature.pose.bones[
|
|
next(bone.name for bone in source_armature.pose.bones if common.simplify_bonename(bone.name) in bone_names['hips']) #Find bone that matches dictionary for hips before continuing.
|
|
]
|
|
)] #bones are default in order of parent child.
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
context.view_layer.objects.active = target_armature
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
for source_bone_name in source_armature_bone_names:
|
|
|
|
if source_bone_name in target_armature_data.edit_bones:
|
|
obj = source_armature
|
|
editbone = target_armature_data.edit_bones[source_bone_name]
|
|
bone = obj.pose.bones[source_bone_name]
|
|
bone.matrix = editbone.matrix
|
|
else:
|
|
continue
|
|
if not common.apply_pose_as_rest(armature_obj=source_armature,meshes=[i for i in source_armature.children if i.type == 'MESH'], context=context):
|
|
cls.report({'ERROR'}, t("Quick_Access.apply_armature_failed"))
|
|
return {'FINISHED'}
|
|
|
|
|
|
|
|
|
|
|
|
cls.make_active(obj=source_armature, context=context)
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
source_armature_data: Armature = source_armature.data
|
|
for bone_name in [i.name for i in source_armature_data.edit_bones]:
|
|
if bone_name in target_armature_data.bones:
|
|
parent_dictionary[bone_name] = [i.name for i in source_armature_data.edit_bones[bone_name].children]
|
|
source_armature_data.edit_bones.remove(source_armature_data.edit_bones[bone_name])
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
cls.make_active(obj=target_armature, context=context)
|
|
source_armature.select_set(True)
|
|
|
|
bpy.ops.object.join()
|
|
target_armature: bpy.types.Object = common.get_selected_armature(context)
|
|
cls.make_active(obj=target_armature, context=context)
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
for bone_name, bone_name_list in parent_dictionary.items():
|
|
if bone_name in target_armature_data.edit_bones:
|
|
for bone_child in bone_name_list:
|
|
target_armature_data.edit_bones[bone_child].parent = target_armature_data.edit_bones[bone_name]
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
|
|
|
|
return {'FINISHED'}
|