2025-12-01
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
from . import (
|
||||
select_similar_curves,
|
||||
lock_curves,
|
||||
bake_anim_across_armatures,
|
||||
)
|
||||
|
||||
modules = [
|
||||
select_similar_curves,
|
||||
lock_curves,
|
||||
bake_anim_across_armatures,
|
||||
]
|
||||
@@ -0,0 +1,84 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from typing import List
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
|
||||
|
||||
def keyed_bones_names(action) -> List[str]:
|
||||
"""Return a list of bone names that have keyframes in the Action of this Slot."""
|
||||
keyed_bones = []
|
||||
for fc in action.fcurves:
|
||||
# Extracting bone name from fcurve data path
|
||||
if "pose.bones" not in fc.data_path:
|
||||
continue
|
||||
bone_name = fc.data_path.split('["')[1].split('"]')[0]
|
||||
|
||||
if bone_name not in keyed_bones:
|
||||
keyed_bones.append(bone_name)
|
||||
|
||||
return keyed_bones
|
||||
|
||||
|
||||
class POSE_OT_bake_anim_across_armatures(Operator):
|
||||
"""Constrain one armature to another, and bake over the animation"""
|
||||
bl_idname = "pose.bake_anim_across_armatures"
|
||||
bl_label = "Bake Animation From Active To Selected Armature"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
if context.mode != 'OBJECT':
|
||||
return False
|
||||
if len(context.selected_objects) != 2:
|
||||
return False
|
||||
if not all([o.type == 'ARMATURE' for o in context.selected_objects]):
|
||||
return False
|
||||
if not context.object in context.selected_objects:
|
||||
return False
|
||||
if not context.object.animation_data and not context.object.animation_data.action:
|
||||
return False
|
||||
return True
|
||||
|
||||
def execute(self, context):
|
||||
action = context.object.animation_data.action
|
||||
src_rig = context.object
|
||||
target_rig = context.selected_objects[0]
|
||||
if src_rig == target_rig:
|
||||
target_rig = context.selected_objects[1]
|
||||
|
||||
bone_layer_backup = target_rig.data.layers[:]
|
||||
# Enable all target rig layers
|
||||
target_rig.data.layers = [True]*32
|
||||
|
||||
# Deselect all target rig bones
|
||||
for b in target_rig.data.bones:
|
||||
b.select = False
|
||||
|
||||
keyed_bones = [src_rig.pose.bones.get(bn) for bn in keyed_bones_names(
|
||||
action) if bn in src_rig.pose.bones]
|
||||
for pb in keyed_bones:
|
||||
# TODO: Bone name mapping based on a passed dictionary.
|
||||
target_bone = target_rig.pose.bones.get(pb.name)
|
||||
if not target_bone:
|
||||
continue
|
||||
|
||||
ct = target_bone.constraints.new(type='COPY_TRANSFORMS')
|
||||
ct.target = src_rig
|
||||
ct.subtarget = target_bone.name
|
||||
target_bone.bone.select = True
|
||||
|
||||
src_rig.select_set(False)
|
||||
bpy.ops.nla.bake(visual_keying=True,
|
||||
clear_constraints=True, bake_types={'POSE'})
|
||||
target_rig.data.layers = bone_layer_backup[:]
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
registry = [
|
||||
POSE_OT_bake_anim_across_armatures,
|
||||
]
|
||||
@@ -0,0 +1,103 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator, Menu
|
||||
|
||||
from bpy.props import EnumProperty, StringProperty, BoolProperty
|
||||
from ..fcurve_utils import get_fcurves
|
||||
|
||||
|
||||
class POSE_OT_curves_set_boolean(Operator):
|
||||
"""Set lock state of all selected curves"""
|
||||
bl_idname = "pose.curves_set_boolean"
|
||||
bl_label = "Set a boolean on curves"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
|
||||
curve_set: EnumProperty(
|
||||
name="Affected Curves",
|
||||
items=[
|
||||
('ACTIVE', 'Active Curve', 'Active Curve'),
|
||||
('SELECTED', 'Selected Curves', 'Selected Curves'),
|
||||
('UNSELECTED', 'Unselected Curves', 'Unselected Curves'),
|
||||
('ALL', 'All Curves', 'All Curves'),
|
||||
],
|
||||
default='SELECTED'
|
||||
)
|
||||
prop_name: StringProperty(
|
||||
name="Property Name",
|
||||
description="Name of the boolean property to set",
|
||||
default='lock'
|
||||
)
|
||||
prop_value: BoolProperty(
|
||||
name="Value",
|
||||
description="Value to set",
|
||||
default=False
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Only works in Graph Editor, when there is an active curve.
|
||||
return context.pose_object and context.pose_object.animation_data and context.pose_object.animation_data.action
|
||||
|
||||
def execute(self, context):
|
||||
action = context.object.animation_data.action
|
||||
affected_fcurves = get_fcurves(context, action, self.curve_set)
|
||||
|
||||
for fc in affected_fcurves:
|
||||
setattr(fc, self.prop_name, self.prop_value)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
class GRAPH_MT_channel_lock(Menu):
|
||||
bl_label = "Lock"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
op = layout.operator(POSE_OT_curves_set_boolean.bl_idname,
|
||||
text="Lock Selected", icon='LOCKED')
|
||||
op.prop_name = "lock"
|
||||
op.prop_value = True
|
||||
op.curve_set = 'SELECTED'
|
||||
|
||||
op = layout.operator(POSE_OT_curves_set_boolean.bl_idname,
|
||||
text="Lock Unselected", icon='LOCKED')
|
||||
op.prop_name = "lock"
|
||||
op.prop_value = True
|
||||
op.curve_set = 'UNSELECTED'
|
||||
|
||||
layout.separator()
|
||||
|
||||
op = layout.operator(POSE_OT_curves_set_boolean.bl_idname,
|
||||
text="Unlock Selected", icon='UNLOCKED')
|
||||
op.prop_name = "lock"
|
||||
op.prop_value = False
|
||||
op.curve_set = 'SELECTED'
|
||||
|
||||
op = layout.operator(POSE_OT_curves_set_boolean.bl_idname,
|
||||
text="Unlock All", icon='UNLOCKED')
|
||||
op.prop_name = "lock"
|
||||
op.prop_value = False
|
||||
op.curve_set = 'ALL'
|
||||
|
||||
|
||||
def draw_curves_lock_menu(self, context):
|
||||
layout = self.layout
|
||||
layout.menu("GRAPH_MT_channel_lock", icon='LOCKED')
|
||||
|
||||
|
||||
registry = [
|
||||
POSE_OT_curves_set_boolean,
|
||||
GRAPH_MT_channel_lock
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.GRAPH_MT_channel.append(draw_curves_lock_menu)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.GRAPH_MT_channel.remove(draw_curves_lock_menu)
|
||||
@@ -0,0 +1,51 @@
|
||||
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.types import Operator
|
||||
from ..fcurve_utils import get_fcurves_of_bones
|
||||
|
||||
|
||||
class POSE_OT_select_matching_curves(Operator):
|
||||
"""Set selection of all curves based on whether they match the transformation axis of the active curve"""
|
||||
bl_idname = "pose.select_matching_curves"
|
||||
bl_label = "Select Matching Curves"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
# Only works in Graph Editor, when there is an active curve.
|
||||
return context.active_editable_fcurve
|
||||
|
||||
def execute(self, context):
|
||||
action = context.object.animation_data.action
|
||||
|
||||
fcurves_of_selected_bones = get_fcurves_of_bones(
|
||||
action, context.selected_pose_bones)
|
||||
|
||||
property_name = context.active_editable_fcurve.data_path.split(".")[-1]
|
||||
|
||||
for fc in fcurves_of_selected_bones:
|
||||
fc.select = fc.data_path.endswith(property_name) and \
|
||||
fc.array_index == context.active_editable_fcurve.array_index
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
|
||||
def draw_select_matching_curves(self, context):
|
||||
layout = self.layout
|
||||
layout.operator(POSE_OT_select_matching_curves.bl_idname)
|
||||
|
||||
|
||||
registry = [
|
||||
POSE_OT_select_matching_curves
|
||||
]
|
||||
|
||||
|
||||
def register():
|
||||
bpy.types.GRAPH_MT_select.append(draw_select_matching_curves)
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.types.GRAPH_MT_select.remove(draw_select_matching_curves)
|
||||
Reference in New Issue
Block a user