# SPDX-License-Identifier: GPL-2.0-or-later # noinspection PyUnresolvedReferences import bpy # noinspection PyUnresolvedReferences import math # noinspection PyUnresolvedReferences from mathutils import Matrix, Vector from typing import Callable, Any, Collection, Iterator from bpy.types import Action, bpy_struct, FCurve import json 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: fcurves = rig.animation_data.action.fcurves else: return False for fc in fcurves: 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 ## ################################################################ # noinspection SpellCheckingInspection 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: return old_matrix tgt_matrix = get_constraint_target_matrix(con) tgt_scale = tgt_matrix.to_scale() use = [con.use_x, con.use_y, con.use_z] if con.use_make_uniform: if con.use_x and con.use_y and con.use_z: total = tgt_matrix.determinant() else: total = 1 for i, use in enumerate(use): if use: total *= tgt_scale[i] tgt_scale = [abs(total)**(1./3.)]*3 else: for i, use in enumerate(use): if not use: tgt_scale[i] = 1 scale_delta = [ 1 / (1 + (math.pow(x, con.power) - 1) * inf) for x in tgt_scale ] 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 ## ############################################ # noinspection SpellCheckingInspection 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) '''] AnyCurveSet = None | FCurve | dict | Collection flatten_curve_set: Callable[[AnyCurveSet], Iterator[FCurve]] flatten_curve_key_set: Callable[..., set[float]] get_curve_frame_set: Callable[..., set[float]] set_curve_key_interpolation: Callable[..., None] delete_curve_keys_in_range: Callable[..., None] nla_tweak_to_scene: Callable find_action: Callable[[bpy_struct], Action] clean_action_empty_curves: Callable[[bpy_struct], None] TRANSFORM_PROPS_LOCATION: frozenset[str] TRANSFORM_PROPS_ROTATION = frozenset[str] TRANSFORM_PROPS_SCALE = frozenset[str] TRANSFORM_PROPS_ALL = frozenset[str] transform_props_with_locks: Callable[[bool, bool, bool], set[str]] FCurveTable: Any ActionCurveTable: Any DriverCurveTable: Any 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 ''' # noinspection SpellCheckingInspection _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='') ''' RIGIFY_OT_get_frame_range: Any exec(_SCRIPT_UTILITIES_BAKE_OPS) ################################################ # Framework for operators that bake keyframes ## ################################################ SCRIPT_REGISTER_BAKE = ['RIGIFY_OT_get_frame_range'] # noinspection SpellCheckingInspection 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) self.keyflags_switch = None 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'} try: self.bake_save_state(context) range, range_raw = self.bake_clean_curves_in_range(context, curves) self.execute_before_apply(context, self.bake_rig, range, range_raw) self.bake_apply_state(context) 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) '''] RigifyOperatorMixinBase: Any RigifyBakeKeyframesMixin: Any RigifySingleUpdateMixin: Any exec(SCRIPT_UTILITIES_BAKE[-1]) ##################################### # Generic Clear Keyframes operator ## ##################################### SCRIPT_REGISTER_OP_CLEAR_KEYS = ['POSE_OT_rigify_clear_keyframes'] # noinspection SpellCheckingInspection 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'} '''] # noinspection PyDefaultArgument,PyUnusedLocal 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'] # noinspection SpellCheckingInspection 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') # noinspection PyDefaultArgument 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.use_bake_settings() panel.script.add_utilities(SCRIPT_UTILITIES_OP_SNAP) panel.script.register_classes(SCRIPT_REGISTER_OP_SNAP) op_props = { '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 add_fk_ik_snap_buttons( 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, ) # noinspection PyDefaultArgument 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)