Skip to content
Snippets Groups Projects
animation.py 34.6 KiB
Newer Older
  • Learn to ignore specific revisions
  • #====================== BEGIN GPL LICENSE BLOCK ======================
    #
    #  This program is free software; you can redistribute it and/or
    #  modify it under the terms of the GNU General Public License
    #  as published by the Free Software Foundation; either version 2
    #  of the License, or (at your option) any later version.
    #
    #  This program is distributed in the hope that it will be useful,
    #  but WITHOUT ANY WARRANTY; without even the implied warranty of
    #  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    #  GNU General Public License for more details.
    #
    #  You should have received a copy of the GNU General Public License
    #  along with this program; if not, write to the Free Software Foundation,
    #  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
    #
    #======================= END GPL LICENSE BLOCK ========================
    
    # <pep8 compliant>
    
    
    import bpy
    
    import math
    import json
    
    from mathutils import Matrix, Vector
    
    rig_id = None
    
    
    #=============================================
    # Keyframing functions
    #=============================================
    
    
    
    def get_keyed_frames_in_range(context, rig):
        action = find_action(rig)
        if action:
            frame_range = RIGIFY_OT_get_frame_range.get_range(context)
    
            return sorted(get_curve_frame_set(action.fcurves, frame_range))
        else:
            return []
    
    
    
    def bones_in_frame(f, rig, *args):
        """
        True if one of the bones listed in args is animated at frame f
        :param f: the frame
        :param rig: the rig
        :param args: bone names
        :return:
        """
    
        if rig.animation_data and rig.animation_data.action:
            fcus = rig.animation_data.action.fcurves
        else:
            return False
    
        for fc in fcus:
            animated_frames = [kp.co[0] for kp in fc.keyframe_points]
            for bone in args:
                if bone in fc.data_path.split('"') and f in animated_frames:
                    return True
    
        return False
    
    
    def overwrite_prop_animation(rig, bone, prop_name, value, frames):
        act = rig.animation_data.action
        if not act:
            return
    
        bone_name = bone.name
        curve = None
    
        for fcu in act.fcurves:
            words = fcu.data_path.split('"')
            if words[0] == "pose.bones[" and words[1] == bone_name and words[-2] == prop_name:
                curve = fcu
                break
    
        if not curve:
            return
    
        for kp in curve.keyframe_points:
            if kp.co[0] in frames:
                kp.co[1] = value
    
    
    ################################################################
    # Utilities for inserting keyframes and/or setting transforms ##
    ################################################################
    
    SCRIPT_UTILITIES_KEYING = ['''
    ######################
    ## Keyframing tools ##
    ######################
    
    def get_keying_flags(context):
        "Retrieve the general keyframing flags from user preferences."
        prefs = context.preferences
        ts = context.scene.tool_settings
        flags = set()
        # Not adding INSERTKEY_VISUAL
        if prefs.edit.use_keyframe_insert_needed:
            flags.add('INSERTKEY_NEEDED')
        if prefs.edit.use_insertkey_xyz_to_rgb:
            flags.add('INSERTKEY_XYZ_TO_RGB')
        if ts.use_keyframe_cycle_aware:
            flags.add('INSERTKEY_CYCLE_AWARE')
        return flags
    
    def get_autokey_flags(context, ignore_keyset=False):
        "Retrieve the Auto Keyframe flags, or None if disabled."
        ts = context.scene.tool_settings
        if ts.use_keyframe_insert_auto and (ignore_keyset or not ts.use_keyframe_insert_keyingset):
            flags = get_keying_flags(context)
            if context.preferences.edit.use_keyframe_insert_available:
                flags.add('INSERTKEY_AVAILABLE')
            if ts.auto_keying_mode == 'REPLACE_KEYS':
                flags.add('INSERTKEY_REPLACE')
            return flags
        else:
            return None
    
    def add_flags_if_set(base, new_flags):
        "Add more flags if base is not None."
        if base is None:
            return None
        else:
            return base | new_flags
    
    def get_4d_rotlock(bone):
        "Retrieve the lock status for 4D rotation."
        if bone.lock_rotations_4d:
            return [bone.lock_rotation_w, *bone.lock_rotation]
        else:
            return [all(bone.lock_rotation)] * 4
    
    def keyframe_transform_properties(obj, bone_name, keyflags, *, ignore_locks=False, no_loc=False, no_rot=False, no_scale=False):
        "Keyframe transformation properties, taking flags and mode into account, and avoiding keying locked channels."
        bone = obj.pose.bones[bone_name]
    
        def keyframe_channels(prop, locks):
            if ignore_locks or not all(locks):
                if ignore_locks or not any(locks):
                    bone.keyframe_insert(prop, group=bone_name, options=keyflags)
                else:
                    for i, lock in enumerate(locks):
                        if not lock:
                            bone.keyframe_insert(prop, index=i, group=bone_name, options=keyflags)
    
        if not (no_loc or bone.bone.use_connect):
            keyframe_channels('location', bone.lock_location)
    
        if not no_rot:
            if bone.rotation_mode == 'QUATERNION':
                keyframe_channels('rotation_quaternion', get_4d_rotlock(bone))
            elif bone.rotation_mode == 'AXIS_ANGLE':
                keyframe_channels('rotation_axis_angle', get_4d_rotlock(bone))
            else:
                keyframe_channels('rotation_euler', bone.lock_rotation)
    
        if not no_scale:
            keyframe_channels('scale', bone.lock_scale)
    
    ######################
    ## Constraint tools ##
    ######################
    
    def get_constraint_target_matrix(con):
        target = con.target
        if target:
            if target.type == 'ARMATURE' and con.subtarget:
                if con.subtarget in target.pose.bones:
                    bone = target.pose.bones[con.subtarget]
                    return target.convert_space(pose_bone=bone, matrix=bone.matrix, from_space='POSE', to_space=con.target_space)
            else:
                return target.convert_space(matrix=target.matrix_world, from_space='WORLD', to_space=con.target_space)
        return Matrix.Identity(4)
    
    def undo_copy_scale_with_offset(obj, bone, con, old_matrix):
        "Undo the effects of Copy Scale with Offset constraint on a bone matrix."
        inf = con.influence
    
        if con.mute or inf == 0 or not con.is_valid or not con.use_offset or con.use_add or con.use_make_uniform:
            return old_matrix
    
        scale_delta = [
            1 / (1 + (math.pow(x, con.power) - 1) * inf)
            for x in get_constraint_target_matrix(con).to_scale()
        ]
    
        for i, use in enumerate([con.use_x, con.use_y, con.use_z]):
            if not use:
                scale_delta[i] = 1
    
        return old_matrix @ Matrix.Diagonal([*scale_delta, 1])
    
    def undo_copy_scale_constraints(obj, bone, matrix):
        "Undo the effects of all Copy Scale with Offset constraints on a bone matrix."
        for con in reversed(bone.constraints):
            if con.type == 'COPY_SCALE':
                matrix = undo_copy_scale_with_offset(obj, bone, con, matrix)
        return matrix
    
    ###############################
    ## Assign and keyframe tools ##
    ###############################
    
    def set_custom_property_value(obj, bone_name, prop, value, *, keyflags=None):
        "Assign the value of a custom property, and optionally keyframe it."
        from rna_prop_ui import rna_idprop_ui_prop_update
        bone = obj.pose.bones[bone_name]
        bone[prop] = value
        rna_idprop_ui_prop_update(bone, prop)
        if keyflags is not None:
            bone.keyframe_insert(rna_idprop_quote_path(prop), group=bone.name, options=keyflags)
    
    def get_transform_matrix(obj, bone_name, *, space='POSE', with_constraints=True):
        "Retrieve the matrix of the bone before or after constraints in the given space."
        bone = obj.pose.bones[bone_name]
        if with_constraints:
            return obj.convert_space(pose_bone=bone, matrix=bone.matrix, from_space='POSE', to_space=space)
        else:
            return obj.convert_space(pose_bone=bone, matrix=bone.matrix_basis, from_space='LOCAL', to_space=space)
    
    def get_chain_transform_matrices(obj, bone_names, **options):
        return [get_transform_matrix(obj, name, **options) for name in bone_names]
    
    def set_transform_from_matrix(obj, bone_name, matrix, *, space='POSE', undo_copy_scale=False, ignore_locks=False, no_loc=False, no_rot=False, no_scale=False, keyflags=None):
        "Apply the matrix to the transformation of the bone, taking locked channels, mode and certain constraints into account, and optionally keyframe it."
        bone = obj.pose.bones[bone_name]
    
        def restore_channels(prop, old_vec, locks, extra_lock):
            if extra_lock or (not ignore_locks and all(locks)):
                setattr(bone, prop, old_vec)
            else:
                if not ignore_locks and any(locks):
                    new_vec = Vector(getattr(bone, prop))
    
                    for i, lock in enumerate(locks):
                        if lock:
                            new_vec[i] = old_vec[i]
    
                    setattr(bone, prop, new_vec)
    
        # Save the old values of the properties
        old_loc = Vector(bone.location)
        old_rot_euler = Vector(bone.rotation_euler)
        old_rot_quat = Vector(bone.rotation_quaternion)
        old_rot_axis = Vector(bone.rotation_axis_angle)
        old_scale = Vector(bone.scale)
    
        # Compute and assign the local matrix
        if space != 'LOCAL':
            matrix = obj.convert_space(pose_bone=bone, matrix=matrix, from_space=space, to_space='LOCAL')
    
        if undo_copy_scale:
            matrix = undo_copy_scale_constraints(obj, bone, matrix)
    
        bone.matrix_basis = matrix
    
        # Restore locked properties
        restore_channels('location', old_loc, bone.lock_location, no_loc or bone.bone.use_connect)
    
        if bone.rotation_mode == 'QUATERNION':
            restore_channels('rotation_quaternion', old_rot_quat, get_4d_rotlock(bone), no_rot)
            bone.rotation_axis_angle = old_rot_axis
            bone.rotation_euler = old_rot_euler
        elif bone.rotation_mode == 'AXIS_ANGLE':
            bone.rotation_quaternion = old_rot_quat
            restore_channels('rotation_axis_angle', old_rot_axis, get_4d_rotlock(bone), no_rot)
            bone.rotation_euler = old_rot_euler
        else:
            bone.rotation_quaternion = old_rot_quat
            bone.rotation_axis_angle = old_rot_axis
            restore_channels('rotation_euler', old_rot_euler, bone.lock_rotation, no_rot)
    
        restore_channels('scale', old_scale, bone.lock_scale, no_scale)
    
        # Keyframe properties
        if keyflags is not None:
            keyframe_transform_properties(
                obj, bone_name, keyflags, ignore_locks=ignore_locks,
                no_loc=no_loc, no_rot=no_rot, no_scale=no_scale
            )
    
    def set_chain_transforms_from_matrices(context, obj, bone_names, matrices, **options):
        for bone, matrix in zip(bone_names, matrices):
            set_transform_from_matrix(obj, bone, matrix, **options)
            context.view_layer.update()
    ''']
    
    exec(SCRIPT_UTILITIES_KEYING[-1])
    
    ############################################
    # Utilities for managing animation curves ##
    ############################################
    
    SCRIPT_UTILITIES_CURVES = ['''
    ###########################
    ## Animation curve tools ##
    ###########################
    
    def flatten_curve_set(curves):
        "Iterate over all FCurves inside a set of nested lists and dictionaries."
        if curves is None:
            pass
        elif isinstance(curves, bpy.types.FCurve):
            yield curves
        elif isinstance(curves, dict):
            for sub in curves.values():
                yield from flatten_curve_set(sub)
        else:
            for sub in curves:
                yield from flatten_curve_set(sub)
    
    def flatten_curve_key_set(curves, key_range=None):
        "Iterate over all keys of the given fcurves in the specified range."
        for curve in flatten_curve_set(curves):
            for key in curve.keyframe_points:
                if key_range is None or key_range[0] <= key.co[0] <= key_range[1]:
                    yield key
    
    def get_curve_frame_set(curves, key_range=None):
        "Compute a set of all time values with existing keys in the given curves and range."
        return set(key.co[0] for key in flatten_curve_key_set(curves, key_range))
    
    def set_curve_key_interpolation(curves, ipo, key_range=None):
        "Assign the given interpolation value to all curve keys in range."
        for key in flatten_curve_key_set(curves, key_range):
            key.interpolation = ipo
    
    def delete_curve_keys_in_range(curves, key_range=None):
        "Delete all keys of the given curves within the given range."
        for curve in flatten_curve_set(curves):
            points = curve.keyframe_points
            for i in range(len(points), 0, -1):
                key = points[i - 1]
                if key_range is None or key_range[0] <= key.co[0] <= key_range[1]:
                    points.remove(key, fast=True)
            curve.update()
    
    def nla_tweak_to_scene(anim_data, frames, invert=False):
        "Convert a frame value or list between scene and tweaked NLA strip time."
        if frames is None:
            return None
        elif anim_data is None or not anim_data.use_tweak_mode:
            return frames
        elif isinstance(frames, (int, float)):
            return anim_data.nla_tweak_strip_time_to_scene(frames, invert=invert)
        else:
            return type(frames)(
                anim_data.nla_tweak_strip_time_to_scene(v, invert=invert) for v in frames
            )
    
    def find_action(action):
        if isinstance(action, bpy.types.Object):
            action = action.animation_data
        if isinstance(action, bpy.types.AnimData):
            action = action.action
        if isinstance(action, bpy.types.Action):
            return action
        else:
            return None
    
    def clean_action_empty_curves(action):
        "Delete completely empty curves from the given action."
        action = find_action(action)
        for curve in list(action.fcurves):
            if curve.is_empty:
                action.fcurves.remove(curve)
        action.update_tag()
    
    TRANSFORM_PROPS_LOCATION = frozenset(['location'])
    TRANSFORM_PROPS_ROTATION = frozenset(['rotation_euler', 'rotation_quaternion', 'rotation_axis_angle'])
    TRANSFORM_PROPS_SCALE = frozenset(['scale'])
    TRANSFORM_PROPS_ALL = frozenset(TRANSFORM_PROPS_LOCATION | TRANSFORM_PROPS_ROTATION | TRANSFORM_PROPS_SCALE)
    
    
    def transform_props_with_locks(lock_location, lock_rotation, lock_scale):
        props = set()
        if not lock_location:
            props |= TRANSFORM_PROPS_LOCATION
        if not lock_rotation:
            props |= TRANSFORM_PROPS_ROTATION
        if not lock_scale:
            props |= TRANSFORM_PROPS_SCALE
        return props
    
    class FCurveTable(object):
    
        "Table for efficient lookup of FCurves by properties."
    
    
        def __init__(self):
            self.curve_map = collections.defaultdict(dict)
    
        def index_curves(self, curves):
            for curve in curves:
    
                index = curve.array_index
                if index < 0:
                    index = 0
                self.curve_map[curve.data_path][index] = curve
    
        def get_prop_curves(self, ptr, prop_path):
            "Returns a dictionary from array index to curve for the given property, or Null."
            return self.curve_map.get(ptr.path_from_id(prop_path))
    
        def list_all_prop_curves(self, ptr_set, path_set):
            "Iterates over all FCurves matching the given object(s) and properti(es)."
            if isinstance(ptr_set, bpy.types.bpy_struct):
                ptr_set = [ptr_set]
            for ptr in ptr_set:
                for path in path_set:
                    curves = self.get_prop_curves(ptr, path)
                    if curves:
                        yield from curves.values()
    
        def get_custom_prop_curves(self, ptr, prop):
            return self.get_prop_curves(ptr, rna_idprop_quote_path(prop))
    
    
    class ActionCurveTable(FCurveTable):
        "Table for efficient lookup of Action FCurves by properties."
    
        def __init__(self, action):
            super().__init__()
            self.action = find_action(action)
            if self.action:
                self.index_curves(self.action.fcurves)
    
    class DriverCurveTable(FCurveTable):
        "Table for efficient lookup of Driver FCurves by properties."
    
        def __init__(self, object):
            super().__init__()
            self.anim_data = object.animation_data
            if self.anim_data:
                self.index_curves(self.anim_data.drivers)
    
    ''']
    
    exec(SCRIPT_UTILITIES_CURVES[-1])
    
    ################################################
    # Utilities for operators that bake keyframes ##
    ################################################
    
    _SCRIPT_REGISTER_WM_PROPS = '''
    bpy.types.WindowManager.rigify_transfer_use_all_keys = bpy.props.BoolProperty(
        name="Bake All Keyed Frames", description="Bake on every frame that has a key for any of the bones, as opposed to just the relevant ones", default=False
    )
    bpy.types.WindowManager.rigify_transfer_use_frame_range = bpy.props.BoolProperty(
        name="Limit Frame Range", description="Only bake keyframes in a certain frame range", default=False
    )
    bpy.types.WindowManager.rigify_transfer_start_frame = bpy.props.IntProperty(
        name="Start", description="First frame to transfer", default=0, min=0
    )
    bpy.types.WindowManager.rigify_transfer_end_frame = bpy.props.IntProperty(
        name="End", description="Last frame to transfer", default=0, min=0
    )
    '''
    
    _SCRIPT_UNREGISTER_WM_PROPS = '''
    del bpy.types.WindowManager.rigify_transfer_use_all_keys
    del bpy.types.WindowManager.rigify_transfer_use_frame_range
    del bpy.types.WindowManager.rigify_transfer_start_frame
    del bpy.types.WindowManager.rigify_transfer_end_frame
    '''
    
    _SCRIPT_UTILITIES_BAKE_OPS = '''
    class RIGIFY_OT_get_frame_range(bpy.types.Operator):
        bl_idname = "rigify.get_frame_range" + ('_'+rig_id if rig_id else '')
        bl_label = "Get Frame Range"
        bl_description = "Set start and end frame from scene"
        bl_options = {'INTERNAL'}
    
        def execute(self, context):
            scn = context.scene
            id_store = context.window_manager
            id_store.rigify_transfer_start_frame = scn.frame_start
            id_store.rigify_transfer_end_frame = scn.frame_end
            return {'FINISHED'}
    
        @staticmethod
        def get_range(context):
            id_store = context.window_manager
            if not id_store.rigify_transfer_use_frame_range:
                return None
            else:
                return (id_store.rigify_transfer_start_frame, id_store.rigify_transfer_end_frame)
    
        @classmethod
        def draw_range_ui(self, context, layout):
            id_store = context.window_manager
    
            row = layout.row(align=True)
            row.prop(id_store, 'rigify_transfer_use_frame_range', icon='PREVIEW_RANGE', text='')
    
            row = row.row(align=True)
            row.active = id_store.rigify_transfer_use_frame_range
            row.prop(id_store, 'rigify_transfer_start_frame')
            row.prop(id_store, 'rigify_transfer_end_frame')
            row.operator(self.bl_idname, icon='TIME', text='')
    '''
    
    exec(_SCRIPT_UTILITIES_BAKE_OPS)
    
    ################################################
    # Framework for operators that bake keyframes ##
    ################################################
    
    SCRIPT_REGISTER_BAKE = ['RIGIFY_OT_get_frame_range']
    
    SCRIPT_UTILITIES_BAKE = SCRIPT_UTILITIES_KEYING + SCRIPT_UTILITIES_CURVES + ['''
    ##################################
    # Common bake operator settings ##
    ##################################
    ''' + _SCRIPT_REGISTER_WM_PROPS + _SCRIPT_UTILITIES_BAKE_OPS + '''
    #######################################
    # Keyframe baking operator framework ##
    #######################################
    
    
    class RigifyOperatorMixinBase:
        bl_options = {'UNDO', 'INTERNAL'}
    
        def init_invoke(self, context):
            "Override to initialize the operator before invoke."
    
        def init_execute(self, context):
            "Override to initialize the operator before execute."
    
        def before_save_state(self, context, rig):
            "Override to prepare for saving state."
    
        def after_save_state(self, context, rig):
            "Override to undo before_save_state."
    
    
    class RigifyBakeKeyframesMixin(RigifyOperatorMixinBase):
    
        """Basic framework for an operator that updates a set of keyed frames."""
    
        # Utilities
        def nla_from_raw(self, frames):
            "Convert frame(s) from inner action time to scene time."
            return nla_tweak_to_scene(self.bake_anim, frames)
    
        def nla_to_raw(self, frames):
            "Convert frame(s) from scene time to inner action time."
            return nla_tweak_to_scene(self.bake_anim, frames, invert=True)
    
        def bake_get_bone(self, bone_name):
            "Get pose bone by name."
            return self.bake_rig.pose.bones[bone_name]
    
        def bake_get_bones(self, bone_names):
            "Get multiple pose bones by name."
            if isinstance(bone_names, (list, set)):
                return [self.bake_get_bone(name) for name in bone_names]
            else:
                return self.bake_get_bone(bone_names)
    
        def bake_get_all_bone_curves(self, bone_names, props):
            "Get a list of all curves for the specified properties of the specified bones."
            return list(self.bake_curve_table.list_all_prop_curves(self.bake_get_bones(bone_names), props))
    
        def bake_get_all_bone_custom_prop_curves(self, bone_names, props):
            "Get a list of all curves for the specified custom properties of the specified bones."
            return self.bake_get_all_bone_curves(bone_names, [rna_idprop_quote_path(p) for p in props])
    
        def bake_get_bone_prop_curves(self, bone_name, prop):
            "Get an index to curve dict for the specified property of the specified bone."
            return self.bake_curve_table.get_prop_curves(self.bake_get_bone(bone_name), prop)
    
        def bake_get_bone_custom_prop_curves(self, bone_name, prop):
            "Get an index to curve dict for the specified custom property of the specified bone."
            return self.bake_curve_table.get_custom_prop_curves(self.bake_get_bone(bone_name), prop)
    
        def bake_add_curve_frames(self, curves):
            "Register frames keyed in the specified curves for baking."
            self.bake_frames_raw |= get_curve_frame_set(curves, self.bake_frame_range_raw)
    
        def bake_add_bone_frames(self, bone_names, props):
            "Register frames keyed for the specified properties of the specified bones for baking."
            curves = self.bake_get_all_bone_curves(bone_names, props)
            self.bake_add_curve_frames(curves)
            return curves
    
        def bake_replace_custom_prop_keys_constant(self, bone, prop, new_value):
            "If the property is keyframed, delete keys in bake range and re-key as Constant."
            prop_curves = self.bake_get_bone_custom_prop_curves(bone, prop)
    
            if prop_curves and 0 in prop_curves:
                range_raw = self.nla_to_raw(self.get_bake_range())
                delete_curve_keys_in_range(prop_curves, range_raw)
                set_custom_property_value(self.bake_rig, bone, prop, new_value, keyflags={'INSERTKEY_AVAILABLE'})
                set_curve_key_interpolation(prop_curves, 'CONSTANT', range_raw)
    
        # Default behavior implementation
        def bake_init(self, context):
            self.bake_rig = context.active_object
            self.bake_anim = self.bake_rig.animation_data
            self.bake_frame_range = RIGIFY_OT_get_frame_range.get_range(context)
            self.bake_frame_range_raw = self.nla_to_raw(self.bake_frame_range)
            self.bake_curve_table = ActionCurveTable(self.bake_rig)
            self.bake_current_frame = context.scene.frame_current
            self.bake_frames_raw = set()
            self.bake_state = dict()
    
            self.keyflags = get_keying_flags(context)
    
    
            if context.window_manager.rigify_transfer_use_all_keys:
                self.bake_add_curve_frames(self.bake_curve_table.curve_map)
    
        def bake_add_frames_done(self):
            "Computes and sets the final set of frames to bake."
            frames = self.nla_from_raw(self.bake_frames_raw)
            self.bake_frames = sorted(set(map(round, frames)))
    
        def is_bake_empty(self):
            return len(self.bake_frames_raw) == 0
    
        def report_bake_empty(self):
            self.bake_add_frames_done()
            if self.is_bake_empty():
                self.report({'WARNING'}, 'No keys to bake.')
                return True
            return False
    
        def get_bake_range(self):
            "Returns the frame range that is being baked."
            if self.bake_frame_range:
                return self.bake_frame_range
            else:
                frames = self.bake_frames
                return (frames[0], frames[-1])
    
        def get_bake_range_pair(self):
            "Returns the frame range that is being baked, both in scene and action time."
            range = self.get_bake_range()
            return range, self.nla_to_raw(range)
    
        def bake_save_state(self, context):
            "Scans frames and collects data for baking before changing anything."
            rig = self.bake_rig
            scene = context.scene
            saved_state = self.bake_state
    
    
            try:
                self.before_save_state(context, rig)
    
                for frame in self.bake_frames:
                    scene.frame_set(frame)
                    saved_state[frame] = self.save_frame_state(context, rig)
    
            finally:
                self.after_save_state(context, rig)
    
    
        def bake_clean_curves_in_range(self, context, curves):
            "Deletes all keys from the given curves in the bake range."
            range, range_raw = self.get_bake_range_pair()
    
            context.scene.frame_set(range[0])
            delete_curve_keys_in_range(curves, range_raw)
    
            return range, range_raw
    
        def bake_apply_state(self, context):
            "Scans frames and applies the baking operation."
            rig = self.bake_rig
            scene = context.scene
            saved_state = self.bake_state
    
            for frame in self.bake_frames:
                scene.frame_set(frame)
                self.apply_frame_state(context, rig, saved_state.get(frame))
    
            clean_action_empty_curves(self.bake_rig)
            scene.frame_set(self.bake_current_frame)
    
        @staticmethod
        def draw_common_bake_ui(context, layout):
            layout.prop(context.window_manager, 'rigify_transfer_use_all_keys')
    
            RIGIFY_OT_get_frame_range.draw_range_ui(context, layout)
    
        @classmethod
        def poll(cls, context):
            return find_action(context.active_object) is not None
    
        def execute_scan_curves(self, context, obj):
            "Override to register frames to be baked, and return curves that should be cleared."
            raise NotImplementedError()
    
        def execute_before_apply(self, context, obj, range, range_raw):
            "Override to execute code one time before the bake apply frame scan."
            pass
    
        def execute(self, context):
            self.init_execute(context)
            self.bake_init(context)
    
            curves = self.execute_scan_curves(context, self.bake_rig)
    
            if self.report_bake_empty():
                return {'CANCELLED'}
    
    
                range, range_raw = self.bake_clean_curves_in_range(context, curves)
    
                self.execute_before_apply(context, self.bake_rig, range, range_raw)
    
            except Exception as e:
                traceback.print_exc()
                self.report({'ERROR'}, 'Exception: ' + str(e))
    
            return {'FINISHED'}
    
    
        def invoke(self, context, event):
            self.init_invoke(context)
    
            if hasattr(self, 'draw'):
                return context.window_manager.invoke_props_dialog(self)
            else:
                return context.window_manager.invoke_confirm(self, event)
    
    
    
    class RigifySingleUpdateMixin(RigifyOperatorMixinBase):
    
        """Basic framework for an operator that updates only the current frame."""
    
        def execute(self, context):
            self.init_execute(context)
            obj = context.active_object
            self.keyflags = get_autokey_flags(context, ignore_keyset=True)
            self.keyflags_switch = add_flags_if_set(self.keyflags, {'INSERTKEY_AVAILABLE'})
    
    
            try:
                try:
                    self.before_save_state(context, obj)
                    state = self.save_frame_state(context, obj)
                finally:
                    self.after_save_state(context, obj)
    
                self.apply_frame_state(context, obj, state)
    
            except Exception as e:
                traceback.print_exc()
                self.report({'ERROR'}, 'Exception: ' + str(e))
    
            return {'FINISHED'}
    
    
        def invoke(self, context, event):
            self.init_invoke(context)
    
            if hasattr(self, 'draw'):
                return context.window_manager.invoke_props_popup(self, event)
            else:
                return self.execute(context)
    ''']
    
    exec(SCRIPT_UTILITIES_BAKE[-1])
    
    #####################################
    # Generic Clear Keyframes operator ##
    #####################################
    
    SCRIPT_REGISTER_OP_CLEAR_KEYS = ['POSE_OT_rigify_clear_keyframes']
    
    SCRIPT_UTILITIES_OP_CLEAR_KEYS = ['''
    #############################
    ## Generic Clear Keyframes ##
    #############################
    
    class POSE_OT_rigify_clear_keyframes(bpy.types.Operator):
        bl_idname = "pose.rigify_clear_keyframes_" + rig_id
        bl_label = "Clear Keyframes And Transformation"
        bl_options = {'UNDO', 'INTERNAL'}
        bl_description = "Remove all keyframes for the relevant bones and reset transformation"
    
        bones: StringProperty(name="Bone List")
    
        @classmethod
        def poll(cls, context):
            return find_action(context.active_object) is not None
    
        def invoke(self, context, event):
            return context.window_manager.invoke_confirm(self, event)
    
        def execute(self, context):
            obj = context.active_object
            bone_list = [ obj.pose.bones[name] for name in json.loads(self.bones) ]
    
            curve_table = ActionCurveTable(context.active_object)
            curves = list(curve_table.list_all_prop_curves(bone_list, TRANSFORM_PROPS_ALL))
    
            key_range = RIGIFY_OT_get_frame_range.get_range(context)
            range_raw = nla_tweak_to_scene(obj.animation_data, key_range, invert=True)
            delete_curve_keys_in_range(curves, range_raw)
    
            for bone in bone_list:
                bone.location = bone.rotation_euler = (0,0,0)
                bone.rotation_quaternion = (1,0,0,0)
                bone.rotation_axis_angle = (0,0,1,0)
                bone.scale = (1,1,1)
    
            clean_action_empty_curves(obj)
            obj.update_tag(refresh={'TIME'})
            return {'FINISHED'}
    ''']
    
    def add_clear_keyframes_button(panel, *, bones=[], label='', text=''):
        panel.use_bake_settings()
        panel.script.add_utilities(SCRIPT_UTILITIES_OP_CLEAR_KEYS)
        panel.script.register_classes(SCRIPT_REGISTER_OP_CLEAR_KEYS)
    
        op_props = { 'bones': json.dumps(bones) }
    
        panel.operator('pose.rigify_clear_keyframes_{rig_id}', text=text, icon='CANCEL', properties=op_props)
    
    
    ###################################
    # Generic Snap FK to IK operator ##
    ###################################
    
    
    SCRIPT_REGISTER_OP_SNAP = ['POSE_OT_rigify_generic_snap', 'POSE_OT_rigify_generic_snap_bake']
    
    SCRIPT_UTILITIES_OP_SNAP = ['''
    #############################
    ## Generic Snap (FK to IK) ##
    #############################
    
    class RigifyGenericSnapBase:
        input_bones:   StringProperty(name="Input Chain")
        output_bones:  StringProperty(name="Output Chain")
        ctrl_bones:    StringProperty(name="Input Controls")
    
        tooltip:         StringProperty(name="Tooltip", default="FK to IK")
        locks:           bpy.props.BoolVectorProperty(name="Locked", size=3, default=[False,False,False])
    
        undo_copy_scale: bpy.props.BoolProperty(name="Undo Copy Scale", default=False)
    
        def init_execute(self, context):
    
            self.input_bone_list = json.loads(self.input_bones)
            self.output_bone_list = json.loads(self.output_bones)
    
            self.ctrl_bone_list = json.loads(self.ctrl_bones)
    
        def save_frame_state(self, context, obj):
    
            return get_chain_transform_matrices(obj, self.input_bone_list)
    
    
        def apply_frame_state(self, context, obj, matrices):
            set_chain_transforms_from_matrices(
    
                context, obj, self.output_bone_list, matrices,
                undo_copy_scale=self.undo_copy_scale, keyflags=self.keyflags,
                no_loc=self.locks[0], no_rot=self.locks[1], no_scale=self.locks[2],
    
    class POSE_OT_rigify_generic_snap(RigifyGenericSnapBase, RigifySingleUpdateMixin, bpy.types.Operator):
        bl_idname = "pose.rigify_generic_snap_" + rig_id
        bl_label = "Snap Bones"
        bl_description = "Snap on the current frame"
    
        @classmethod
        def description(cls, context, props):
            return "Snap " + props.tooltip + " on the current frame"
    
    class POSE_OT_rigify_generic_snap_bake(RigifyGenericSnapBase, RigifyBakeKeyframesMixin, bpy.types.Operator):
        bl_idname = "pose.rigify_generic_snap_bake_" + rig_id
        bl_label = "Apply Snap To Keyframes"
        bl_description = "Apply snap to keyframes"
    
        @classmethod
        def description(cls, context, props):
            return "Apply snap " + props.tooltip + " to keyframes"
    
    
        def execute_scan_curves(self, context, obj):
    
            props = transform_props_with_locks(*self.locks)
    
            self.bake_add_bone_frames(self.ctrl_bone_list, TRANSFORM_PROPS_ALL)
    
            return self.bake_get_all_bone_curves(self.output_bone_list, props)
    
    ''']
    
    def add_fk_ik_snap_buttons(panel, op_single, op_bake, *, label=None, rig_name='', properties=None, clear_bones=None, compact=None):
        assert label and properties
    
        if rig_name:
            label += ' (%s)' % (rig_name)
    
        if compact or not clear_bones:
            row = panel.row(align=True)
            row.operator(op_single, text=label, icon='SNAP_ON', properties=properties)
            row.operator(op_bake, text='', icon='ACTION_TWEAK', properties=properties)
    
            if clear_bones:
                add_clear_keyframes_button(row, bones=clear_bones)
        else:
            col = panel.column(align=True)
            col.operator(op_single, text=label, icon='SNAP_ON', properties=properties)
            row = col.row(align=True)
            row.operator(op_bake, text='Action', icon='ACTION_TWEAK', properties=properties)
            add_clear_keyframes_button(row, bones=clear_bones, text='Clear')
    
    
    def add_generic_snap(panel, *, output_bones=[], input_bones=[], input_ctrl_bones=[], label='Snap', rig_name='', undo_copy_scale=False, compact=None, clear=True, locks=None, tooltip=None):
    
        panel.script.add_utilities(SCRIPT_UTILITIES_OP_SNAP)
        panel.script.register_classes(SCRIPT_REGISTER_OP_SNAP)
    
            'output_bones': json.dumps(output_bones),
            'input_bones': json.dumps(input_bones),
            'ctrl_bones': json.dumps(input_ctrl_bones or input_bones),
    
        if undo_copy_scale:
            op_props['undo_copy_scale'] = undo_copy_scale
        if locks is not None:
            op_props['locks'] = tuple(locks[0:3])
        if tooltip is not None:
            op_props['tooltip'] = tooltip
    
        clear_bones = output_bones if clear else None
    
            panel, 'pose.rigify_generic_snap_{rig_id}', 'pose.rigify_generic_snap_bake_{rig_id}',
    
            label=label, rig_name=rig_name, properties=op_props, clear_bones=clear_bones, compact=compact,
        )
    
    
    def add_generic_snap_fk_to_ik(panel, *, fk_bones=[], ik_bones=[], ik_ctrl_bones=[], label='FK->IK', rig_name='', undo_copy_scale=False, compact=None, clear=True):
        add_generic_snap(
            panel, output_bones=fk_bones, input_bones=ik_bones, input_ctrl_bones=ik_ctrl_bones,
            label=label, rig_name=rig_name, undo_copy_scale=undo_copy_scale, compact=compact, clear=clear
        )
    
    
    ###############################
    # Module register/unregister ##
    ###############################
    
    def register():
        from bpy.utils import register_class
    
        exec(_SCRIPT_REGISTER_WM_PROPS)
    
        register_class(RIGIFY_OT_get_frame_range)
    
    def unregister():
        from bpy.utils import unregister_class
    
        exec(_SCRIPT_UNREGISTER_WM_PROPS)
    
        unregister_class(RIGIFY_OT_get_frame_range)