Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAlexander Gavrilov <angavrilov@gmail.com>2019-03-30 22:00:55 +0300
committerAlexander Gavrilov <angavrilov@gmail.com>2019-09-14 09:29:26 +0300
commit3423174b37a0784dc12035ff3f2fb536835099e1 (patch)
tree3a54580902cdebdef5ebacd6099e86cc79ba75b3 /rigify/utils
parent12af8a28c14b608e9b9b08568d981273c86590c1 (diff)
Rigify: redesign generate.py and introduce a base rig class.
The main goals are to provide an official way for rigs to interact in a structured way, and to remove mode switching within rigs. This involves introducing a base class for rigs that holds rig-to-rig and rig-to-bone references, converting the main generator into a class and passing it to rigs, and splitting the single generate method into multiple passes. For backward compatibility, old rigs are automatically handled via a wrapper that translates between old and new API. In addition, a way to create objects that receive the generate callbacks that aren't rigs is introduced via the GeneratorPlugin class. The UI script generation code is converted into a plugin. Making generic rig 'template' classes that are intended to be subclassed in specific rigs involves splitting operations done in each stage into multiple methods that can be overridden separately. The main callback thus ends up simply calling a sequence of other methods. To make such code cleaner it's better to allow registering those methods as new callbacks that would be automatically called by the system. This can be done via decorators. A new metaclass used for all rig and generate plugin classes builds and validates a table of all decorated methods, and allows calling them all together with the main callback. A new way to switch parents for IK bones based on the new features is introduced, and used in the existing limb rigs. Reviewers: icappiello campbellbarton Differential Revision: https://developer.blender.org/D4624
Diffstat (limited to 'rigify/utils')
-rw-r--r--rigify/utils/__init__.py9
-rw-r--r--rigify/utils/animation.py818
-rw-r--r--rigify/utils/bones.py332
-rw-r--r--rigify/utils/errors.py11
-rw-r--r--rigify/utils/layers.py9
-rw-r--r--rigify/utils/mechanism.py35
-rw-r--r--rigify/utils/metaclass.py171
-rw-r--r--rigify/utils/misc.py67
-rw-r--r--rigify/utils/naming.py154
-rw-r--r--rigify/utils/rig.py53
-rw-r--r--rigify/utils/switch_parent.py438
-rw-r--r--rigify/utils/widgets_basic.py4
12 files changed, 1992 insertions, 109 deletions
diff --git a/rigify/utils/__init__.py b/rigify/utils/__init__.py
index 9fa6a3d2..f45acded 100644
--- a/rigify/utils/__init__.py
+++ b/rigify/utils/__init__.py
@@ -11,7 +11,7 @@ from .naming import strip_trailing_number, unique_name, org_name, strip_org, str
from .naming import org, make_original_name, mch, make_mechanism_name, deformer, make_deformer_name
from .naming import insert_before_lr, random_id
-from .bones import new_bone, copy_bone_simple, copy_bone, flip_bone, put_bone, make_nonscaling_child
+from .bones import new_bone, flip_bone, put_bone
from .bones import align_bone_roll, align_bone_x_axis, align_bone_z_axis, align_bone_y_axis
from .widgets import WGT_PREFIX, obj_to_bone, create_widget, write_widget, create_circle_polygon
@@ -22,10 +22,13 @@ from .widgets_basic import create_sphere_widget, create_limb_widget, create_bone
from .widgets_special import create_compass_widget, create_root_widget
from .widgets_special import create_neck_bend_widget, create_neck_tweak_widget
-from .animation import get_keyed_frames, bones_in_frame, overwrite_prop_animation
-
from .rig import RIG_DIR, METARIG_DIR, TEMPLATE_DIR, outdated_types, upgradeMetarigTypes
from .rig import write_metarig, get_resource
from .rig import connected_children_names, has_connected_children
from .layers import get_layers, ControlLayersOption
+
+# Definitions so bad as to make them strictly compatibility only
+from .bones import copy_bone as copy_bone_simple
+from .bones import _legacy_copy_bone as copy_bone
+from .bones import _legacy_make_nonscaling_child as make_nonscaling_child
diff --git a/rigify/utils/animation.py b/rigify/utils/animation.py
index ab99282f..62042923 100644
--- a/rigify/utils/animation.py
+++ b/rigify/utils/animation.py
@@ -18,25 +18,28 @@
# <pep8 compliant>
+import bpy
+
+import math
+import json
+
+from mathutils import Matrix, Vector
+
+rig_id = None
#=============================================
# Keyframing functions
#=============================================
-def get_keyed_frames(rig):
- frames = []
- if rig.animation_data:
- if rig.animation_data.action:
- fcus = rig.animation_data.action.fcurves
- for fc in fcus:
- for kp in fc.keyframe_points:
- if kp.co[0] not in frames:
- frames.append(kp.co[0])
+def get_keyed_frames_in_range(context, rig):
+ action = find_action(rig)
+ if action:
+ frame_range = RIGIFY_OT_get_frame_range.get_range(context)
- frames.sort()
-
- return frames
+ return sorted(get_curve_frame_set(action.fcurves, frame_range))
+ else:
+ return []
def bones_in_frame(f, rig, *args):
@@ -82,3 +85,794 @@ def overwrite_prop_animation(rig, bone, prop_name, value, frames):
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)
+
+class ActionCurveTable(object):
+ "Table for efficient lookup of FCurves by properties."
+
+ def __init__(self, action):
+ from collections import defaultdict
+ self.action = find_action(action)
+ self.curve_map = defaultdict(dict)
+ self.index_action()
+
+ def index_action(self):
+ if not self.action:
+ return
+
+ for curve in self.action.fcurves:
+ 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))
+''']
+
+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 RigifyBakeKeyframesMixin:
+ """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
+
+ for frame in self.bake_frames:
+ scene.frame_set(frame)
+ saved_state[frame] = self.save_frame_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 init_execute(self, context):
+ "Override to initialize the operator."
+ 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'}
+
+ 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)
+ return {'FINISHED'}
+
+ def init_invoke(self, context):
+ "Override to initialize the operator."
+ pass
+
+ 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:
+ """Basic framework for an operator that updates only the current frame."""
+
+ def init_execute(self, context):
+ pass
+
+ 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'})
+ self.apply_frame_state(context, obj, self.save_frame_state(context, obj))
+ return {'FINISHED'}
+
+ def init_invoke(self, context):
+ pass
+
+ 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_FK_IK = ['POSE_OT_rigify_generic_fk2ik', 'POSE_OT_rigify_generic_fk2ik_bake']
+
+SCRIPT_UTILITIES_OP_SNAP_FK_IK = ['''
+###########################
+## Generic Snap FK to IK ##
+###########################
+
+class RigifyGenericFk2IkBase:
+ fk_bones: StringProperty(name="FK Bone Chain")
+ ik_bones: StringProperty(name="IK Result Bone Chain")
+ ctrl_bones: StringProperty(name="IK Controls")
+
+ undo_copy_scale: bpy.props.BoolProperty(name="Undo Copy Scale", default=False)
+
+ keyflags = None
+
+ def init_execute(self, context):
+ self.fk_bone_list = json.loads(self.fk_bones)
+ self.ik_bone_list = json.loads(self.ik_bones)
+ self.ctrl_bone_list = json.loads(self.ctrl_bones)
+
+ def save_frame_state(self, context, obj):
+ return get_chain_transform_matrices(obj, self.ik_bone_list)
+
+ def apply_frame_state(self, context, obj, matrices):
+ set_chain_transforms_from_matrices(
+ context, obj, self.fk_bone_list, matrices,
+ undo_copy_scale=self.undo_copy_scale, keyflags=self.keyflags
+ )
+
+class POSE_OT_rigify_generic_fk2ik(RigifyGenericFk2IkBase, RigifySingleUpdateMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_generic_fk2ik_" + rig_id
+ bl_label = "Snap FK->IK"
+ bl_options = {'UNDO', 'INTERNAL'}
+ bl_description = "Snap the FK chain to IK result"
+
+class POSE_OT_rigify_generic_fk2ik_bake(RigifyGenericFk2IkBase, RigifyBakeKeyframesMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_generic_fk2ik_bake_" + rig_id
+ bl_label = "Apply Snap FK->IK To Keyframes"
+ bl_options = {'UNDO', 'INTERNAL'}
+ bl_description = "Snap the FK chain keyframes to IK result"
+
+ def execute_scan_curves(self, context, obj):
+ self.bake_add_bone_frames(self.ctrl_bone_list, TRANSFORM_PROPS_ALL)
+ return self.bake_get_all_bone_curves(self.fk_bone_list, TRANSFORM_PROPS_ALL)
+''']
+
+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_fk_to_ik(panel, *, fk_bones=[], ik_bones=[], ik_ctrl_bones=[], label='FK->IK', rig_name='', undo_copy_scale=False, compact=None, clear=True):
+ panel.use_bake_settings()
+ panel.script.add_utilities(SCRIPT_UTILITIES_OP_SNAP_FK_IK)
+ panel.script.register_classes(SCRIPT_REGISTER_OP_SNAP_FK_IK)
+
+ op_props = {
+ 'fk_bones': json.dumps(fk_bones),
+ 'ik_bones': json.dumps(ik_bones),
+ 'ctrl_bones': json.dumps(ik_ctrl_bones),
+ 'undo_copy_scale': undo_copy_scale,
+ }
+
+ clear_bones = fk_bones if clear else None
+
+ add_fk_ik_snap_buttons(
+ panel, 'pose.rigify_generic_fk2ik_{rig_id}', 'pose.rigify_generic_fk2ik_bake_{rig_id}',
+ label=label, rig_name=rig_name, properties=op_props, clear_bones=clear_bones, compact=compact,
+ )
+
+###############################
+# 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)
diff --git a/rigify/utils/bones.py b/rigify/utils/bones.py
index 136ece7d..b5559a76 100644
--- a/rigify/utils/bones.py
+++ b/rigify/utils/bones.py
@@ -24,7 +24,8 @@ from mathutils import Vector, Matrix, Color
from rna_prop_ui import rna_idprop_ui_prop_get
from .errors import MetarigError
-from .naming import make_derived_name
+from .naming import get_name, make_derived_name
+from .misc import pairwise
#=======================
# Bone collection
@@ -55,7 +56,7 @@ class BoneDict(dict):
raise ValueError("Invalid BoneDict value: %r" % (value))
def __init__(self, *args, **kwargs):
- super(BoneDict, self).__init__()
+ super().__init__()
for key, value in dict(*args, **kwargs).items():
dict.__setitem__(self, key, BoneDict.__sanitize_attr(key, value))
@@ -72,12 +73,14 @@ class BoneDict(dict):
for key, value in dict(*args, **kwargs).items():
dict.__setitem__(self, key, BoneDict.__sanitize_attr(key, value))
- def flatten(self):
- """Return all contained bones as a list."""
+ def flatten(self, key=None):
+ """Return all contained bones or a single key as a list."""
+
+ items = [self[key]] if key is not None else self.values()
all_bones = []
- for item in self.values():
+ for item in items:
if isinstance(item, BoneDict):
all_bones.extend(item.flatten())
elif isinstance(item, list):
@@ -90,6 +93,18 @@ class BoneDict(dict):
#=======================
# Bone manipulation
#=======================
+#
+# NOTE: PREFER USING BoneUtilityMixin IN NEW STYLE RIGS!
+
+def get_bone(obj, bone_name):
+ """Get EditBone or PoseBone by name, depending on the current mode."""
+ if not bone_name:
+ return None
+ bones = obj.data.edit_bones if obj.mode == 'EDIT' else obj.pose.bones
+ if bone_name not in bones:
+ raise MetarigError("bone '%s' not found" % bone_name)
+ return bones[bone_name]
+
def new_bone(obj, bone_name):
""" Adds a new bone to the given armature object.
@@ -101,17 +116,14 @@ def new_bone(obj, bone_name):
edit_bone.head = (0, 0, 0)
edit_bone.tail = (0, 1, 0)
edit_bone.roll = 0
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='EDIT')
return name
else:
raise MetarigError("Can't add new bone '%s' outside of edit mode" % bone_name)
-def copy_bone_simple(obj, bone_name, assign_name=''):
+def copy_bone(obj, bone_name, assign_name='', *, parent=False, bbone=False, length=None, scale=None):
""" Makes a copy of the given bone in the given armature object.
- but only copies head, tail positions and roll. Does not
- address parenting either.
+ Returns the resulting bone's name.
"""
#if bone_name not in obj.data.bones:
if bone_name not in obj.data.edit_bones:
@@ -133,49 +145,36 @@ def copy_bone_simple(obj, bone_name, assign_name=''):
edit_bone_2.tail = Vector(edit_bone_1.tail)
edit_bone_2.roll = edit_bone_1.roll
- return bone_name_2
- else:
- raise MetarigError("Cannot copy bones outside of edit mode")
-
-
-def copy_bone(obj, bone_name, assign_name=''):
- """ Makes a copy of the given bone in the given armature object.
- Returns the resulting bone's name.
- """
- #if bone_name not in obj.data.bones:
- if bone_name not in obj.data.edit_bones:
- raise MetarigError("copy_bone(): bone '%s' not found, cannot copy it" % bone_name)
+ if parent:
+ edit_bone_2.parent = edit_bone_1.parent
+ edit_bone_2.use_connect = edit_bone_1.use_connect
- if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE':
- if assign_name == '':
- assign_name = bone_name
- # Copy the edit bone
- edit_bone_1 = obj.data.edit_bones[bone_name]
- edit_bone_2 = obj.data.edit_bones.new(assign_name)
- bone_name_1 = bone_name
- bone_name_2 = edit_bone_2.name
+ edit_bone_2.use_inherit_rotation = edit_bone_1.use_inherit_rotation
+ edit_bone_2.use_inherit_scale = edit_bone_1.use_inherit_scale
+ edit_bone_2.use_local_location = edit_bone_1.use_local_location
- edit_bone_2.parent = edit_bone_1.parent
- edit_bone_2.use_connect = edit_bone_1.use_connect
+ if bbone:
+ for name in ['bbone_segments',
+ 'bbone_easein', 'bbone_easeout',
+ 'bbone_rollin', 'bbone_rollout',
+ 'bbone_curveinx', 'bbone_curveiny', 'bbone_curveoutx', 'bbone_curveouty',
+ 'bbone_scaleinx', 'bbone_scaleiny', 'bbone_scaleoutx', 'bbone_scaleouty']:
+ setattr(edit_bone_2, name, getattr(edit_bone_1, name))
- # Copy edit bone attributes
- edit_bone_2.layers = list(edit_bone_1.layers)
+ # Resize the bone after copy if requested
+ if length is not None:
+ edit_bone_2.length = length
+ elif scale is not None:
+ edit_bone_2.length *= scale
- edit_bone_2.head = Vector(edit_bone_1.head)
- edit_bone_2.tail = Vector(edit_bone_1.tail)
- edit_bone_2.roll = edit_bone_1.roll
-
- edit_bone_2.use_inherit_rotation = edit_bone_1.use_inherit_rotation
- edit_bone_2.use_inherit_scale = edit_bone_1.use_inherit_scale
- edit_bone_2.use_local_location = edit_bone_1.use_local_location
-
- edit_bone_2.use_deform = edit_bone_1.use_deform
- edit_bone_2.bbone_segments = edit_bone_1.bbone_segments
- edit_bone_2.bbone_easein = edit_bone_1.bbone_easein
- edit_bone_2.bbone_easeout = edit_bone_1.bbone_easeout
+ return bone_name_2
+ else:
+ raise MetarigError("Cannot copy bones outside of edit mode")
- bpy.ops.object.mode_set(mode='OBJECT')
+def copy_bone_properties(obj, bone_name_1, bone_name_2):
+ """ Copy transform and custom properties from bone 1 to bone 2. """
+ if obj.mode in {'OBJECT','POSE'}:
# Get the pose bones
pose_bone_1 = obj.pose.bones[bone_name_1]
pose_bone_2 = obj.pose.bones[bone_name_2]
@@ -203,18 +202,24 @@ def copy_bone(obj, bone_name, assign_name=''):
prop2 = rna_idprop_ui_prop_get(pose_bone_2, key, create=True)
for key in prop1.keys():
prop2[key] = prop1[key]
+ else:
+ raise MetarigError("Cannot copy bone properties in edit mode")
- bpy.ops.object.mode_set(mode='EDIT')
- return bone_name_2
- else:
- raise MetarigError("Cannot copy bones outside of edit mode")
+def _legacy_copy_bone(obj, bone_name, assign_name=''):
+ """LEGACY ONLY, DON'T USE"""
+ new_name = copy_bone(obj, bone_name, assign_name, parent=True, bbone=True)
+ # Mode switch PER BONE CREATION?!
+ bpy.ops.object.mode_set(mode='OBJECT')
+ copy_bone_properties(obj, bone_name, new_name)
+ bpy.ops.object.mode_set(mode='EDIT')
+ return new_name
def flip_bone(obj, bone_name):
""" Flips an edit bone.
"""
- if bone_name not in obj.data.bones:
+ if bone_name not in obj.data.edit_bones:
raise MetarigError("flip_bone(): bone '%s' not found, cannot copy it" % bone_name)
if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE':
@@ -228,10 +233,38 @@ def flip_bone(obj, bone_name):
raise MetarigError("Cannot flip bones outside of edit mode")
+def flip_bone_chain(obj, bone_names):
+ """Flips a connected bone chain."""
+ assert obj.mode == 'EDIT'
+
+ bones = [ obj.data.edit_bones[name] for name in bone_names ]
+
+ # Verify chain and unparent
+ for prev_bone, bone in pairwise(bones):
+ assert bone.parent == prev_bone and bone.use_connect
+
+ for bone in bones:
+ bone.parent = None
+ bone.use_connect = False
+ for child in bone.children:
+ child.use_connect = False
+
+ # Flip bones
+ for bone in bones:
+ head, tail = Vector(bone.head), Vector(bone.tail)
+ bone.tail = head + tail
+ bone.head, bone.tail = tail, head
+
+ # Re-parent
+ for bone, next_bone in pairwise(bones):
+ bone.parent = next_bone
+ bone.use_connect = True
+
+
def put_bone(obj, bone_name, pos):
""" Places a bone at the given position.
"""
- if bone_name not in obj.data.bones:
+ if bone_name not in obj.data.edit_bones:
raise MetarigError("put_bone(): bone '%s' not found, cannot move it" % bone_name)
if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE':
@@ -243,7 +276,14 @@ def put_bone(obj, bone_name, pos):
raise MetarigError("Cannot 'put' bones outside of edit mode")
-def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""):
+def disable_bbones(obj, bone_names):
+ """Disables B-Bone segments on the specified bones."""
+ assert(obj.mode != 'EDIT')
+ for bone in bone_names:
+ obj.data.bones[bone].bbone_segments = 1
+
+
+def _legacy_make_nonscaling_child(obj, bone_name, location, child_name_postfix=""):
""" Takes the named bone and creates a non-scaling child of it at
the given location. The returned bone (returned by name) is not
a true child, but behaves like one sans inheriting scaling.
@@ -251,8 +291,10 @@ def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""):
It is intended as an intermediate construction to prevent rig types
from scaling with their parents. The named bone is assumed to be
an ORG bone.
+
+ LEGACY ONLY, DON'T USE
"""
- if bone_name not in obj.data.bones:
+ if bone_name not in obj.data.edit_bones:
raise MetarigError("make_nonscaling_child(): bone '%s' not found, cannot copy it" % bone_name)
if obj == bpy.context.active_object and bpy.context.mode == 'EDIT_ARMATURE':
@@ -305,11 +347,129 @@ def make_nonscaling_child(obj, bone_name, location, child_name_postfix=""):
raise MetarigError("Cannot make nonscaling child outside of edit mode")
+#===================================
+# Bone manipulation as rig methods
+#===================================
+
+
+class BoneUtilityMixin(object):
+ """
+ Provides methods for more convenient creation of bones.
+
+ Requires self.obj to be the armature object being worked on.
+ """
+ def register_new_bone(self, new_name, old_name=None):
+ """Registers creation or renaming of a bone based on old_name"""
+ pass
+
+ def new_bone(self, new_name):
+ """Create a new bone with the specified name."""
+ name = new_bone(self.obj, bone_name)
+ self.register_new_bone(self, name)
+ return name
+
+ def copy_bone(self, bone_name, new_name='', *, parent=False, bbone=False, length=None, scale=None):
+ """Copy the bone with the given name, returning the new name."""
+ name = copy_bone(self.obj, bone_name, new_name, parent=parent, bbone=bbone, length=length, scale=scale)
+ self.register_new_bone(name, bone_name)
+ return name
+
+ def copy_bone_properties(self, src_name, tgt_name):
+ """Copy pose-mode properties of the bone."""
+ copy_bone_properties(self.obj, src_name, tgt_name)
+
+ def rename_bone(self, old_name, new_name):
+ """Rename the bone, returning the actual new name."""
+ bone = self.get_bone(old_name)
+ bone.name = new_name
+ if bone.name != old_name:
+ self.register_new_bone(bone.name, old_name)
+ return bone.name
+
+ def get_bone(self, bone_name):
+ """Get EditBone or PoseBone by name, depending on the current mode."""
+ return get_bone(self.obj, bone_name)
+
+ def get_bone_parent(self, bone_name):
+ """Get the name of the parent bone, or None."""
+ return get_name(self.get_bone(bone_name).parent)
+
+ def set_bone_parent(self, bone_name, parent_name, use_connect=False):
+ """Set the parent of the bone."""
+ eb = self.obj.data.edit_bones
+ bone = eb[bone_name]
+ if use_connect is not None:
+ bone.use_connect = use_connect
+ bone.parent = (eb[parent_name] if parent_name else None)
+
+ def parent_bone_chain(self, bone_names, use_connect=None):
+ """Link bones into a chain with parenting. First bone may be None."""
+ for parent, child in pairwise(bone_names):
+ self.set_bone_parent(child, parent, use_connect=use_connect)
+
+#=============================================
+# B-Bones
+#=============================================
+
+def connect_bbone_chain_handles(obj, bone_names):
+ assert obj.mode == 'EDIT'
+
+ for prev_name, next_name in pairwise(bone_names):
+ prev_bone = get_bone(obj, prev_name)
+ next_bone = get_bone(obj, next_name)
+
+ prev_bone.bbone_handle_type_end = 'ABSOLUTE'
+ prev_bone.bbone_custom_handle_end = next_bone
+
+ next_bone.bbone_handle_type_start = 'ABSOLUTE'
+ next_bone.bbone_custom_handle_start = prev_bone
+
#=============================================
# Math
#=============================================
+def is_same_position(obj, bone_name1, bone_name2):
+ head1 = get_bone(obj, bone_name1).head
+ head2 = get_bone(obj, bone_name2).head
+
+ return (head1 - head2).length < 1e-5
+
+
+def is_connected_position(obj, bone_name1, bone_name2):
+ tail1 = get_bone(obj, bone_name1).tail
+ head2 = get_bone(obj, bone_name2).head
+
+ return (tail1 - head2).length < 1e-5
+
+
+def copy_bone_position(obj, bone_name, target_bone_name, *, length=None, scale=None):
+ """ Completely copies the position and orientation of the bone. """
+ bone1_e = obj.data.edit_bones[bone_name]
+ bone2_e = obj.data.edit_bones[target_bone_name]
+
+ bone2_e.head = bone1_e.head
+ bone2_e.tail = bone1_e.tail
+ bone2_e.roll = bone1_e.roll
+
+ # Resize the bone after copy if requested
+ if length is not None:
+ bone2_e.length = length
+ elif scale is not None:
+ bone2_e.length *= scale
+
+
+def align_bone_orientation(obj, bone_name, target_bone_name):
+ """ Aligns the orientation of bone to target bone. """
+ bone1_e = obj.data.edit_bones[bone_name]
+ bone2_e = obj.data.edit_bones[target_bone_name]
+
+ axis = bone2_e.y_axis.normalized() * bone1_e.length
+
+ bone1_e.tail = bone1_e.head + axis
+ bone1_e.roll = bone2_e.roll
+
+
def align_bone_roll(obj, bone1, bone2):
""" Aligns the roll of two bones.
"""
@@ -416,3 +576,65 @@ def align_bone_y_axis(obj, bone, vec):
vec = vec * bone_e.length
bone_e.tail = bone_e.head + vec
+
+
+def compute_chain_x_axis(obj, bone_names):
+ """
+ Compute the x axis of all bones to be perpendicular
+ to the primary plane in which the bones lie.
+ """
+ eb = obj.data.edit_bones
+
+ assert(len(bone_names) > 1)
+ first_bone = eb[bone_names[0]]
+ last_bone = eb[bone_names[-1]]
+
+ # Compute normal to the plane defined by the first bone,
+ # and the end of the last bone in the chain
+ chain_y_axis = last_bone.tail - first_bone.head
+ chain_rot_axis = first_bone.y_axis.cross(chain_y_axis)
+
+ if chain_rot_axis.length < first_bone.length/100:
+ return first_bone.x_axis.normalized()
+ else:
+ return chain_rot_axis.normalized()
+
+
+def align_chain_x_axis(obj, bone_names):
+ """
+ Aligns the x axis of all bones to be perpendicular
+ to the primary plane in which the bones lie.
+ """
+ chain_rot_axis = compute_chain_x_axis(obj, bone_names)
+
+ for name in bone_names:
+ align_bone_x_axis(obj, name, chain_rot_axis)
+
+
+def align_bone_to_axis(obj, bone_name, axis, *, length=None, roll=0, flip=False):
+ """
+ Aligns the Y axis of the bone to the global axis (x,y,z,-x,-y,-z),
+ optionally adjusting length and initially flipping the bone.
+ """
+ bone_e = obj.data.edit_bones[bone_name]
+
+ if length is None:
+ length = bone_e.length
+ if roll is None:
+ roll = bone_e.roll
+
+ if axis[0] == '-':
+ length = -length
+ axis = axis[1:]
+
+ vec = Vector((0,0,0))
+ setattr(vec, axis, length)
+
+ if flip:
+ base = Vector(bone_e.tail)
+ bone_e.tail = base + vec
+ bone_e.head = base
+ else:
+ bone_e.tail = bone_e.head + vec
+
+ bone_e.roll = roll
diff --git a/rigify/utils/errors.py b/rigify/utils/errors.py
index 71295057..0fc81ccb 100644
--- a/rigify/utils/errors.py
+++ b/rigify/utils/errors.py
@@ -32,3 +32,14 @@ class MetarigError(Exception):
def __str__(self):
return repr(self.message)
+
+class RaiseErrorMixin(object):
+ def raise_error(self, message, *args, **kwargs):
+ from .naming import strip_org
+
+ message = message.format(*args, **kwargs)
+
+ if hasattr(self, 'base_bone'):
+ message = "Bone '%s': %s" % (strip_org(self.base_bone), message)
+
+ raise MetarigError("RIGIFY ERROR: " + message)
diff --git a/rigify/utils/layers.py b/rigify/utils/layers.py
index 1045e493..b624e9ac 100644
--- a/rigify/utils/layers.py
+++ b/rigify/utils/layers.py
@@ -21,6 +21,12 @@
import bpy
+ORG_LAYER = [n == 31 for n in range(0, 32)] # Armature layer that original bones should be moved to.
+MCH_LAYER = [n == 30 for n in range(0, 32)] # Armature layer that mechanism bones should be moved to.
+DEF_LAYER = [n == 29 for n in range(0, 32)] # Armature layer that deformation bones should be moved to.
+ROOT_LAYER = [n == 28 for n in range(0, 32)] # Armature layer that root bone should be moved to.
+
+
def get_layers(layers):
""" Does its best to extract a set of layers from any data thrown at it.
"""
@@ -69,6 +75,9 @@ class ControlLayersOption:
def assign(self, params, bone_set, bone_list):
layers = self.get(params)
+ if isinstance(bone_set, bpy.types.Object):
+ bone_set = bone_set.data.bones
+
if layers:
for name in bone_list:
bone = bone_set[name]
diff --git a/rigify/utils/mechanism.py b/rigify/utils/mechanism.py
index ee1e3dfc..937e07e9 100644
--- a/rigify/utils/mechanism.py
+++ b/rigify/utils/mechanism.py
@@ -32,9 +32,13 @@ _TRACK_AXIS_MAP = {
'Z': 'TRACK_Z', '-Z': 'TRACK_NEGATIVE_Z',
}
+def _set_default_attr(obj, options, attr, value):
+ if hasattr(obj, attr):
+ options.setdefault(attr, value)
+
def make_constraint(
owner, type, target=None, subtarget=None, *,
- space=None, track_axis=None, use_xyz=None,
+ space=None, track_axis=None, use_xyz=None, use_limit_xyz=None,
**options):
"""
Creates and initializes constraint of the specified type for the owner bone.
@@ -45,7 +49,8 @@ def make_constraint(
space : assigned to both owner_space and target_space
track_axis : allows shorter X, Y, Z, -X, -Y, -Z notation
use_xyz : list of 3 items is assigned to use_x, use_y and use_z options
- min/max_x/y/z : a corresponding use_min/max_x/y/z option is set to True
+ use_limit_xyz : list of 3 items is assigned to use_limit_x/y/z options
+ min/max_x/y/z : a corresponding use_(min/max/limit)_(x/y/z) option is set to True
Other keyword arguments are directly assigned to the constraint options.
Returns the newly created constraint.
@@ -59,7 +64,8 @@ def make_constraint(
con.subtarget = subtarget
if space is not None:
- con.owner_space = con.target_space = space
+ _set_default_attr(con, options, 'owner_space', space)
+ _set_default_attr(con, options, 'target_space', space)
if track_axis is not None:
con.track_axis = _TRACK_AXIS_MAP.get(track_axis, track_axis)
@@ -67,9 +73,13 @@ def make_constraint(
if use_xyz is not None:
con.use_x, con.use_y, con.use_z = use_xyz[0:3]
+ if use_limit_xyz is not None:
+ con.use_limit_x, con.use_limit_y, con.use_limit_z = use_limit_xyz[0:3]
+
for key in ['min_x', 'max_x', 'min_y', 'max_y', 'min_z', 'max_z']:
- if key in options and 'use_'+key not in options:
- options['use_'+key] = True
+ if key in options:
+ _set_default_attr(con, options, 'use_'+key, True)
+ _set_default_attr(con, options, 'use_limit_'+key[-1], True)
for p, v in options.items():
setattr(con, p, v)
@@ -125,7 +135,10 @@ def _init_driver_target(drv_target, var_info, target_id):
# Use ".foo" type path items verbatim, otherwise quote
path = subtarget.path_from_id()
for item in refs:
- path += item if item[0] == '.' else '["'+item+'"]'
+ if isinstance(item, str):
+ path += item if item[0] == '.' else '["'+item+'"]'
+ else:
+ path += '[%r]' % (item)
drv_target.id = target_id
drv_target.data_path = path
@@ -223,6 +236,14 @@ def make_driver(owner, prop, *, index=-1, type='SUM', expression=None, variables
else:
drv.type = type
+ # In case the driver already existed, remove contents
+ for var in list(drv.variables):
+ drv.variables.remove(var)
+
+ for mod in list(fcu.modifiers):
+ fcu.modifiers.remove(mod)
+
+ # Fill in new data
if isinstance(variables, list):
# variables = [ info, ... ]
for i, var_info in enumerate(variables):
@@ -234,7 +255,7 @@ def make_driver(owner, prop, *, index=-1, type='SUM', expression=None, variables
_add_driver_variable(drv, var_name, var_info, target_id)
if polynomial is not None:
- drv_modifier = fcu.modifiers[0]
+ drv_modifier = fcu.modifiers.new('GENERATOR')
drv_modifier.mode = 'POLYNOMIAL'
drv_modifier.poly_order = len(polynomial)-1
for i,v in enumerate(polynomial):
diff --git a/rigify/utils/metaclass.py b/rigify/utils/metaclass.py
new file mode 100644
index 00000000..77ce4b6b
--- /dev/null
+++ b/rigify/utils/metaclass.py
@@ -0,0 +1,171 @@
+#====================== 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 collections
+
+from types import FunctionType
+from itertools import chain
+
+
+#=============================================
+# Class With Stages
+#=============================================
+
+
+def rigify_stage(stage):
+ """Decorates the method with the specified stage."""
+ def process(method):
+ if not isinstance(method, FunctionType):
+ raise ValueError("Stage decorator must be applied to a method definition")
+ method._rigify_stage = stage
+ return method
+ return process
+
+
+class StagedMetaclass(type):
+ """
+ Metaclass for rigs that manages assignment of methods to stages via @stage.* decorators.
+
+ Using define_stages=True in the class definition will register all non-system
+ method names from that definition as valid stages. After that, subclasses can
+ register methods to those stages, to be called via rigify_invoke_stage.
+ """
+ def __new__(metacls, class_name, bases, namespace, define_stages=None, **kwds):
+ # suppress keyword args to avoid issues with __init_subclass__
+ return super().__new__(metacls, class_name, bases, namespace, **kwds)
+
+ def __init__(self, class_name, bases, namespace, define_stages=None, **kwds):
+ super().__init__(class_name, bases, namespace, **kwds)
+
+ # Compute the set of stages defined by this class
+ if not define_stages:
+ define_stages = []
+
+ elif define_stages is True:
+ define_stages = [
+ name for name, item in namespace.items()
+ if name[0] != '_' and isinstance(item, FunctionType)
+ ]
+
+ self.rigify_own_stages = frozenset(define_stages)
+
+ # Compute complete set of inherited stages
+ staged_bases = [ cls for cls in reversed(self.__mro__) if isinstance(cls, StagedMetaclass) ]
+
+ self.rigify_stages = stages = frozenset(chain.from_iterable(
+ cls.rigify_own_stages for cls in staged_bases
+ ))
+
+ # Compute the inherited stage to method mapping
+ stage_map = collections.defaultdict(collections.OrderedDict)
+ own_stage_map = collections.defaultdict(collections.OrderedDict)
+ method_map = {}
+
+ self.rigify_own_stage_map = own_stage_map
+
+ for base in staged_bases:
+ for stage_name, methods in base.rigify_own_stage_map.items():
+ for method_name, method_class in methods.items():
+ if method_name in stages:
+ raise ValueError("Stage method '%s' inherited @stage.%s in class %s (%s)" %
+ (method_name, stage_name, class_name, self.__module__))
+
+ # Check consistency of inherited stage assignment to methods
+ if method_name in method_map:
+ if method_map[method_name] != stage_name:
+ print("RIGIFY CLASS %s (%s): method '%s' has inherited both @stage.%s and @stage.%s\n" %
+ (class_name, self.__module__, method_name, method_map[method_name], stage_name))
+ else:
+ method_map[method_name] = stage_name
+
+ stage_map[stage_name][method_name] = method_class
+
+ # Scan newly defined methods for stage decorations
+ for method_name, item in namespace.items():
+ if isinstance(item, FunctionType):
+ stage = getattr(item, '_rigify_stage', None)
+
+ if stage and method_name in stages:
+ print("RIGIFY CLASS %s (%s): cannot use stage decorator on the stage method '%s' (@stage.%s ignored)" %
+ (class_name, self.__module__, method_name, stage))
+ continue
+
+ # Ensure that decorators aren't lost when redefining methods
+ if method_name in method_map:
+ if not stage:
+ stage = method_map[method_name]
+ print("RIGIFY CLASS %s (%s): missing stage decorator on method '%s' (should be @stage.%s)" %
+ (class_name, self.__module__, method_name, stage))
+ # Check that the method is assigned to only one stage
+ elif stage != method_map[method_name]:
+ print("RIGIFY CLASS %s (%s): method '%s' has decorator @stage.%s, but inherited base has @stage.%s" %
+ (class_name, self.__module__, method_name, stage, method_map[method_name]))
+
+ # Assign the method to the stage, verifying that it's valid
+ if stage:
+ if stage not in stages:
+ raise ValueError("Invalid stage name '%s' for method '%s' in class %s (%s)" %
+ (stage, method_name, class_name, self.__module__))
+ else:
+ stage_map[stage][method_name] = self
+ own_stage_map[stage][method_name] = self
+
+ self.rigify_stage_map = stage_map
+
+ def make_stage_decorators(self):
+ return [(name, rigify_stage(name)) for name in self.rigify_stages]
+
+
+class BaseStagedClass(object, metaclass=StagedMetaclass):
+ rigify_sub_objects = tuple()
+
+ def rigify_invoke_stage(self, stage):
+ """Call all methods decorated with the given stage, followed by the callback."""
+ cls = self.__class__
+ assert isinstance(cls, StagedMetaclass)
+ assert stage in cls.rigify_stages
+
+ getattr(self, stage)()
+
+ for sub in self.rigify_sub_objects:
+ sub.rigify_invoke_stage(stage)
+
+ for method_name in cls.rigify_stage_map[stage]:
+ getattr(self, method_name)()
+
+
+#=============================================
+# Per-owner singleton class
+#=============================================
+
+
+class SingletonPluginMetaclass(StagedMetaclass):
+ """Metaclass for maintaining one instance per owner object per constructor arg set."""
+ def __call__(cls, owner, *constructor_args):
+ key = (cls, *constructor_args)
+ try:
+ return owner.plugin_map[key]
+ except KeyError:
+ new_obj = super().__call__(owner, *constructor_args)
+ owner.plugin_map[key] = new_obj
+ owner.plugin_list.append(new_obj)
+ owner.plugin_list.sort(key=lambda obj: obj.priority, reverse=True)
+ return new_obj
+
diff --git a/rigify/utils/misc.py b/rigify/utils/misc.py
index 2ca7b016..b0f79ea7 100644
--- a/rigify/utils/misc.py
+++ b/rigify/utils/misc.py
@@ -19,8 +19,12 @@
# <pep8 compliant>
import math
+import collections
+
+from itertools import tee, chain, islice, repeat
from mathutils import Vector, Matrix, Color
+
#=============================================
# Math
#=============================================
@@ -82,6 +86,49 @@ def gamma_correct(color):
#=============================================
+# Iterators
+#=============================================
+
+
+def padnone(iterable, pad=None):
+ return chain(iterable, repeat(pad))
+
+
+def pairwise_nozip(iterable):
+ "s -> (s0,s1), (s1,s2), (s2,s3), ..."
+ a, b = tee(iterable)
+ next(b, None)
+ return a, b
+
+
+def pairwise(iterable):
+ "s -> (s0,s1), (s1,s2), (s2,s3), ..."
+ a, b = tee(iterable)
+ next(b, None)
+ return zip(a, b)
+
+
+def map_list(func, *inputs):
+ "[func(a0,b0...), func(a1,b1...), ...]"
+ return list(map(func, *inputs))
+
+
+def skip(n, iterable):
+ "Returns an iterator skipping first n elements of an iterable."
+ iterator = iter(iterable)
+ if n == 1:
+ next(iterator, None)
+ else:
+ next(islice(iterator, n, n), None)
+ return iterator
+
+
+def map_apply(func, *inputs):
+ "Apply the function to inputs like map for side effects, discarding results."
+ collections.deque(map(func, *inputs), maxlen=0)
+
+
+#=============================================
# Misc
#=============================================
@@ -98,3 +145,23 @@ def copy_attributes(a, b):
setattr(b, key, getattr(a, key))
except AttributeError:
pass
+
+
+def assign_parameters(target, val_dict=None, **params):
+ data = { **val_dict, **params } if val_dict else params
+ for key, value in data.items():
+ try:
+ target[key] = value
+ except Exception as e:
+ raise Exception("Couldn't set {} to {}: {}".format(key,value,e))
+
+
+def select_object(context, object, deselect_all=False):
+ view_layer = context.view_layer
+
+ if deselect_all:
+ for objt in view_layer.objects:
+ objt.select_set(False) # deselect all objects
+
+ object.select_set(True)
+ view_layer.objects.active = object
diff --git a/rigify/utils/naming.py b/rigify/utils/naming.py
index 3983704a..d2fa90a3 100644
--- a/rigify/utils/naming.py
+++ b/rigify/utils/naming.py
@@ -21,34 +21,150 @@
import random
import time
import re
+import collections
+import enum
ORG_PREFIX = "ORG-" # Prefix of original bones.
MCH_PREFIX = "MCH-" # Prefix of mechanism bones.
DEF_PREFIX = "DEF-" # Prefix of deformation bones.
ROOT_NAME = "root" # Name of the root bone.
+_PREFIX_TABLE = { 'org': "ORG", 'mch': "MCH", 'def': "DEF", 'ctrl': '' }
+
+#=======================================================================
+# Name structure
+#=======================================================================
+
+NameParts = collections.namedtuple('NameParts', ['prefix', 'base', 'side', 'number'])
+
+
+def split_name(name):
+ name_parts = re.match(r'^(?:(ORG|MCH|DEF)-)?(.*?)([._-][lLrR])?(?:\.(\d+))?$', name)
+ return NameParts(*name_parts.groups())
+
+
+def combine_name(parts, *, prefix=None, base=None, side=None, number=None):
+ eff_prefix = prefix if prefix is not None else parts.prefix
+ eff_number = number if number is not None else parts.number
+ if isinstance(eff_number, int):
+ eff_number = '%03d' % (eff_number)
+
+ return ''.join([
+ eff_prefix+'-' if eff_prefix else '',
+ base if base is not None else parts.base,
+ side if side is not None else parts.side or '',
+ '.'+eff_number if eff_number else '',
+ ])
+
+
+def insert_before_lr(name, text):
+ parts = split_name(name)
+
+ if parts.side:
+ return combine_name(parts, base=parts.base + text)
+ else:
+ return name + text
+
+
+def make_derived_name(name, subtype, suffix=None):
+ """ Replaces the name prefix, and optionally adds the suffix (before .LR if found).
+ """
+ assert(subtype in _PREFIX_TABLE)
+
+ parts = split_name(name)
+ new_base = parts.base + (suffix or '')
+
+ return combine_name(parts, prefix=_PREFIX_TABLE[subtype], base=new_base)
+
+
+#=======================================================================
+# Name mirroring
+#=======================================================================
+
+class Side(enum.IntEnum):
+ LEFT = -1
+ MIDDLE = 0
+ RIGHT = 1
+
+ @staticmethod
+ def from_parts(parts):
+ if parts.side:
+ if parts.side[1].lower() == 'l':
+ return Side.LEFT
+ else:
+ return Side.RIGHT
+ else:
+ return Side.MIDDLE
+
+ @staticmethod
+ def to_string(parts, side):
+ if side != Side.MIDDLE:
+ side_char = 'L' if side == Side.LEFT else 'R'
+
+ if parts.side:
+ sep, schar = parts.side[0:2]
+ if schar.lower() == schar:
+ side_char = side_char.lower()
+ else:
+ sep = '.'
+
+ return sep + side_char
+ else:
+ return ''
+
+ @staticmethod
+ def to_name(parts, side):
+ new_side = Side.to_string(parts, side)
+ return combine_name(parts, side=new_side)
+
+
+def get_name_side(name):
+ return Side.from_parts(split_name(name))
+
+
+def get_name_side_and_base(name):
+ parts = split_name(name)
+ return Side.from_parts(parts), Side.to_name(parts, side=Side.MIDDLE)
+
+
+def change_name_side(name, side):
+ return Side.to_name(split_name(name), side)
+
+
+def mirror_name(name):
+ parts = split_name(name)
+ side = Side.from_parts(parts)
+
+ if side != Side.MIDDLE:
+ return Side.to_name(parts, -side)
+ else:
+ return name
+
#=======================================================================
# Name manipulation
#=======================================================================
+def get_name(bone):
+ return bone.name if bone else None
+
-def strip_trailing_number(s):
- m = re.search(r'\.(\d{3})$', s)
- return s[0:-4] if m else s
+def strip_trailing_number(name):
+ return combine_name(split_name(name), number='')
def strip_prefix(name):
- return re.sub(r'^(?:ORG|MCH|DEF)-', '', name)
+ return combine_name(split_name(name), prefix='')
def unique_name(collection, base_name):
- base_name = strip_trailing_number(base_name)
+ parts = split_name(base_name)
+ name = combine_name(parts, number='')
count = 1
- name = base_name
- while collection.get(name):
- name = "%s.%03d" % (base_name, count)
+ while name in collection:
+ name = combine_name(parts, number=count)
count += 1
+
return name
@@ -120,28 +236,6 @@ def deformer(name):
make_deformer_name = deformer
-_prefix_functions = { 'org': org, 'mch': mch, 'def': deformer, 'ctrl': lambda x: x }
-
-
-def insert_before_lr(name, text):
- name_parts = re.match(r'^(.*?)((?:[._-][lLrR](?:\.\d+)?)?)$', name)
- name_base, name_suffix = name_parts.groups()
- return name_base + text + name_suffix
-
-
-def make_derived_name(name, subtype, suffix=None):
- """ Replaces the name prefix, and optionally adds the suffix (before .LR if found).
- """
- assert(subtype in _prefix_functions)
-
- name = strip_prefix(name)
-
- if suffix:
- name = insert_before_lr(name, suffix)
-
- return _prefix_functions[subtype](name)
-
-
def random_id(length=8):
""" Generates a random alphanumeric id string.
"""
diff --git a/rigify/utils/rig.py b/rigify/utils/rig.py
index 414ea133..41027c69 100644
--- a/rigify/utils/rig.py
+++ b/rigify/utils/rig.py
@@ -50,6 +50,12 @@ outdated_types = {"pitchipoy.limbs.super_limb": "limbs.super_limb",
"spine": ""
}
+def get_rigify_type(pose_bone):
+ return pose_bone.rigify_type.replace(" ", "")
+
+def is_rig_base_bone(obj, name):
+ return bool(get_rigify_type(obj.pose.bones[name]))
+
def upgradeMetarigTypes(metarig, revert=False):
"""Replaces rigify_type properties from old versions with their current names
@@ -89,6 +95,34 @@ def get_resource(resource_name):
return module
+def attach_persistent_script(obj, script):
+ """Make sure the ui script always follows the rig around"""
+ skip = False
+ driver = None
+
+ if not obj.animation_data:
+ obj.animation_data_create()
+
+ for fcurve in obj.animation_data.drivers:
+ if fcurve.data_path == 'pass_index':
+ driver = fcurve.driver
+ for variable in driver.variables:
+ if variable.name == script.name:
+ skip = True
+ break
+ break
+
+ if not skip:
+ if not driver:
+ fcurve = obj.driver_add("pass_index")
+ driver = fcurve.driver
+
+ variable = driver.variables.new()
+ variable.name = script.name
+ variable.targets[0].id_type = 'TEXT'
+ variable.targets[0].id = script
+
+
def connected_children_names(obj, bone_name):
""" Returns a list of bone names (in order) of the bones that form a single
connected chain starting with the given bone as a parent.
@@ -124,6 +158,23 @@ def has_connected_children(bone):
return t
+def _list_bone_names_depth_first_sorted_rec(result_list, bone):
+ result_list.append(bone.name)
+
+ for child in sorted(list(bone.children), key=lambda b: b.name):
+ _list_bone_names_depth_first_sorted_rec(result_list, child)
+
+def list_bone_names_depth_first_sorted(obj):
+ """Returns a list of bone names in depth first name sorted order."""
+ result_list = []
+
+ for bone in sorted(list(obj.data.bones), key=lambda b: b.name):
+ if bone.parent is None:
+ _list_bone_names_depth_first_sorted_rec(result_list, bone)
+
+ return result_list
+
+
def write_metarig(obj, layers=False, func_name="create", groups=False):
"""
Write a metarig as a python script, this rig is to have all info needed for
@@ -248,6 +299,8 @@ def write_metarig(obj, layers=False, func_name="create", groups=False):
code.append("\n arm.layers = [(x in " + str(active_layers) + ") for x in range(" + str(len(arm.layers)) + ")]")
+ code.append("\n return bones")
+
code.append('\nif __name__ == "__main__":')
code.append(" " + func_name + "(bpy.context.active_object)\n")
diff --git a/rigify/utils/switch_parent.py b/rigify/utils/switch_parent.py
new file mode 100644
index 00000000..5daa6a6c
--- /dev/null
+++ b/rigify/utils/switch_parent.py
@@ -0,0 +1,438 @@
+import bpy
+
+import re
+import itertools
+import bisect
+import json
+
+from .errors import MetarigError
+from .naming import strip_prefix, make_derived_name
+from .mechanism import MechanismUtilityMixin
+from .misc import map_list, map_apply
+
+from ..base_rig import *
+from ..base_generate import GeneratorPlugin
+
+from itertools import count, repeat
+
+def _auto_call(value):
+ if callable(value):
+ return value()
+ else:
+ return value
+
+def _rig_is_child(rig, parent):
+ if parent is None:
+ return True
+
+ while rig:
+ if rig is parent:
+ return True
+
+ rig = rig.rigify_parent
+
+ return False
+
+
+class SwitchParentBuilder(GeneratorPlugin, MechanismUtilityMixin):
+ """
+ Implements centralized generation of switchable parent mechanisms.
+ Allows all rigs to register their bones as possible parents for other rigs.
+ """
+
+ def __init__(self, generator):
+ super().__init__(generator)
+
+ self.child_list = []
+ self.global_parents = []
+ self.local_parents = []
+ self.child_map = {}
+ self.frozen = False
+
+ self.register_parent(None, 'root', name='Root', is_global=True)
+
+
+ ##############################
+ # API
+
+ def register_parent(self, rig, bone, *, name=None, is_global=False, exclude_self=False):
+ """
+ Registers a bone of the specified rig as a possible parent.
+
+ Parameters:
+ rig Owner of the bone.
+ bone Actual name of the parent bone.
+ name Name of the parent for mouse-over hint.
+ is_global The parent is accessible to all rigs, instead of just children of owner.
+ exclude_self The parent is invisible to the owner rig itself.
+
+ Lazy creation:
+ The bone parameter may be a function creating the bone on demand and
+ returning its name. It is guaranteed to be called at most once.
+ """
+
+ assert not self.frozen
+ assert isinstance(bone, str) or callable(bone)
+
+ entry = {
+ 'rig': rig, 'bone': bone, 'name': name,
+ 'is_global': is_global, 'exclude_self': exclude_self, 'used': False,
+ }
+
+ if is_global:
+ self.global_parents.append(entry)
+ else:
+ self.local_parents.append(entry)
+
+
+ def build_child(self, rig, bone, *, use_parent_mch=True, **options):
+ """
+ Build a switchable parent mechanism for the specified bone.
+
+ Parameters:
+ rig Owner of the child bone.
+ bone Name of the child bone.
+ extra_parents List of bone names or (name, user_name) pairs to use as additional parents.
+ use_parent_mch Create an intermediate MCH bone for the constraints and parent the child to it.
+ select_parent Select the specified bone instead of the last one.
+ ignore_global Ignore the is_global flag of potential parents.
+ context_rig Rig to use for selecting parents.
+
+ prop_bone Name of the bone to add the property to.
+ prop_id Actual name of the control property.
+ prop_name Name of the property to use in the UI script.
+ controls Collection of controls to bind property UI to.
+
+ ctrl_bone User visible control bone that depends on this parent (for switch & keep transform)
+ no_fix_* Disable "Switch and Keep Transform" correction for specific channels.
+ copy_* Override the specified components by copying from another bone.
+
+ Lazy parameters:
+ 'extra_parents', 'select_parent', 'prop_bone', 'controls', 'copy_*'
+ may be a function returning the value. They are called in the configure_bones stage.
+ """
+ assert self.generator.stage == 'generate_bones' and not self.frozen
+ assert rig is not None
+ assert isinstance(bone, str)
+ assert bone not in self.child_map
+
+ # Create MCH proxy
+ if use_parent_mch:
+ mch_bone = rig.copy_bone(bone, make_derived_name(bone, 'mch', '.parent'), scale=1/3)
+ else:
+ mch_bone = bone
+
+ child = {
+ **self.child_option_table,
+ 'rig':rig, 'bone': bone, 'mch_bone': mch_bone,
+ 'is_done': False, 'is_configured': False,
+ }
+ self.assign_child_options(child, options)
+ self.child_list.append(child)
+ self.child_map[bone] = child
+
+
+ def amend_child(self, rig, bone, **options):
+ """
+ Change parameters assigned in a previous build_child call.
+
+ Provided to make it more convenient to change rig behavior by subclassing.
+ """
+ assert self.generator.stage == 'generate_bones' and not self.frozen
+ child = self.child_map[bone]
+ assert child['rig'] == rig
+ self.assign_child_options(child, options)
+
+
+ def rig_child_now(self, bone):
+ """Create the constraints immediately."""
+ assert self.generator.stage == 'rig_bones'
+ child = self.child_map[bone]
+ assert not child['is_done']
+ self.__rig_child(child)
+
+ ##############################
+ # Implementation
+
+ child_option_table = {
+ 'extra_parents': None,
+ 'prop_bone': None, 'prop_id': None, 'prop_name': None, 'controls': None,
+ 'select_parent': None, 'ignore_global': False, 'context_rig': None,
+ 'ctrl_bone': None,
+ 'no_fix_location': False, 'no_fix_rotation': False, 'no_fix_scale': False,
+ 'copy_location': None, 'copy_rotation': None, 'copy_scale': None,
+ }
+
+ def assign_child_options(self, child, options):
+ if 'context_rig' in options:
+ assert _rig_is_child(child['rig'], options['context_rig'])
+
+ for name, value in options.items():
+ if name not in self.child_option_table:
+ raise AttributeError('invalid child option: '+name)
+
+ child[name] = value
+
+ def generate_bones(self):
+ self.frozen = True
+ self.parent_list = self.global_parents + self.local_parents
+
+ # Link children to parents
+ for child in self.child_list:
+ child_rig = child['context_rig'] or child['rig']
+ parents = []
+
+ for parent in self.parent_list:
+ if parent['rig'] is child_rig:
+ if parent['exclude_self']:
+ continue
+ elif parent['is_global'] and not child['ignore_global']:
+ # Can't use parents from own children, even if global (cycle risk)
+ if _rig_is_child(parent['rig'], child_rig):
+ continue
+ else:
+ # Required to be a child of the parent's rig
+ if not _rig_is_child(child_rig, parent['rig']):
+ continue
+
+ parent['used'] = True
+ parents.append(parent)
+
+ child['parents'] = parents
+
+ # Call lazy creation for parents
+ for parent in self.parent_list:
+ if parent['used']:
+ parent['bone'] = _auto_call(parent['bone'])
+
+ def parent_bones(self):
+ for child in self.child_list:
+ rig = child['rig']
+ mch = child['mch_bone']
+
+ # Remove real parent from the child
+ rig.set_bone_parent(mch, None)
+ self.generator.disable_auto_parent(mch)
+
+ # Parent child to the MCH proxy
+ if mch != child['bone']:
+ rig.set_bone_parent(child['bone'], mch)
+
+ def configure_bones(self):
+ for child in self.child_list:
+ self.__configure_child(child)
+
+ def __configure_child(self, child):
+ if child['is_configured']:
+ return
+
+ child['is_configured'] = True
+
+ bone = child['bone']
+
+ # Build the final list of parent bone names
+ parent_map = dict()
+
+ for parent in child['parents']:
+ if parent['bone'] not in parent_map:
+ parent_map[parent['bone']] = parent['name']
+
+ last_main_parent_bone = child['parents'][-1]['bone']
+ num_main_parents = len(parent_map.items())
+
+ for parent in _auto_call(child['extra_parents'] or []):
+ if not isinstance(parent, tuple):
+ parent = (parent, None)
+ if parent[0] not in parent_map:
+ parent_map[parent[0]] = parent[1]
+
+ parent_bones = list(parent_map.items())
+ child['parent_bones'] = parent_bones
+
+ # Find which bone to select
+ select_bone = _auto_call(child['select_parent']) or last_main_parent_bone
+ select_index = num_main_parents
+
+ try:
+ select_index = 1 + next(i for i, (bone, _) in enumerate(parent_bones) if bone == select_bone)
+ except StopIteration:
+ print("RIGIFY ERROR: Can't find bone '%s' to select as default parent of '%s'\n" % (select_bone, bone))
+
+ # Create the controlling property
+ prop_bone = child['prop_bone'] = _auto_call(child['prop_bone']) or bone
+ prop_name = child['prop_name'] or child['prop_id'] or 'Parent Switch'
+ prop_id = child['prop_id'] = child['prop_id'] or 'parent_switch'
+
+ parent_names = [ parent[1] or strip_prefix(parent[0]) for parent in [(None, 'None'), *parent_bones] ]
+ parent_str = ', '.join([ '%s (%d)' % (name, i) for i, name in enumerate(parent_names) ])
+
+ ctrl_bone = child['ctrl_bone'] or bone
+
+ self.make_property(
+ prop_bone, prop_id, select_index,
+ min=0, max=len(parent_bones),
+ description='Switch parent of %s: %s' % (ctrl_bone, parent_str)
+ )
+
+ # Find which channels don't depend on the parent
+
+ no_fix = [ child[n] for n in ['no_fix_location', 'no_fix_rotation', 'no_fix_scale'] ]
+
+ child['copy'] = [ _auto_call(child[n]) for n in ['copy_location', 'copy_rotation', 'copy_scale'] ]
+
+ locks = tuple(bool(nofix or copy) for nofix, copy in zip(no_fix, child['copy']))
+
+ # Create the script for the property
+ controls = _auto_call(child['controls']) or set([prop_bone, bone])
+
+ script = self.generator.script
+ panel = script.panel_with_selected_check(child['rig'], controls)
+
+ panel.use_bake_settings()
+ script.add_utilities(SCRIPT_UTILITIES_OP_SWITCH_PARENT)
+ script.register_classes(SCRIPT_REGISTER_OP_SWITCH_PARENT)
+
+ op_props = {
+ 'bone': ctrl_bone, 'prop_bone': prop_bone, 'prop_id': prop_id,
+ 'parent_names': json.dumps(parent_names), 'locks': locks,
+ }
+
+ row = panel.row(align=True)
+ lsplit = row.split(factor=0.75, align=True)
+ lsplit.operator('pose.rigify_switch_parent_{rig_id}', text=prop_name, icon='DOWNARROW_HLT', properties=op_props)
+ lsplit.custom_prop(prop_bone, prop_id, text='')
+ row.operator('pose.rigify_switch_parent_bake_{rig_id}', text='', icon='ACTION_TWEAK', properties=op_props)
+
+ def rig_bones(self):
+ for child in self.child_list:
+ self.__rig_child(child)
+
+ def __rig_child(self, child):
+ if child['is_done']:
+ return
+
+ child['is_done'] = True
+
+ # Implement via an Armature constraint
+ mch = child['mch_bone']
+ con = self.make_constraint(mch, 'ARMATURE', name='SWITCH_PARENT')
+
+ prop_var = [(child['prop_bone'], child['prop_id'])]
+
+ for i, (parent, parent_name) in enumerate(child['parent_bones']):
+ tgt = con.targets.new()
+
+ tgt.target = self.obj
+ tgt.subtarget = parent
+ tgt.weight = 0.0
+
+ expr = 'var == %d' % (i+1)
+ self.make_driver(tgt, 'weight', expression=expr, variables=prop_var)
+
+ # Add copy constraints
+ copy = child['copy']
+
+ if copy[0]:
+ self.make_constraint(mch, 'COPY_LOCATION', copy[0])
+ if copy[1]:
+ self.make_constraint(mch, 'COPY_ROTATION', copy[1])
+ if copy[2]:
+ self.make_constraint(mch, 'COPY_SCALE', copy[2])
+
+
+SCRIPT_REGISTER_OP_SWITCH_PARENT = ['POSE_OT_rigify_switch_parent', 'POSE_OT_rigify_switch_parent_bake']
+
+SCRIPT_UTILITIES_OP_SWITCH_PARENT = ['''
+################################
+## Switchable Parent operator ##
+################################
+
+class RigifySwitchParentBase:
+ bone: StringProperty(name="Control Bone")
+ prop_bone: StringProperty(name="Property Bone")
+ prop_id: StringProperty(name="Property")
+ parent_names: StringProperty(name="Parent Names")
+ locks: bpy.props.BoolVectorProperty(name="Locked", size=3, default=[False,False,False])
+
+ parent_items = [('0','None','None')]
+
+ selected: bpy.props.EnumProperty(
+ name='Selected Parent',
+ items=lambda s,c: RigifySwitchParentBase.parent_items
+ )
+
+ keyflags = None
+ keyflags_switch = None
+
+ def save_frame_state(self, context, obj):
+ return get_transform_matrix(obj, self.bone, with_constraints=False)
+
+ def apply_frame_state(self, context, obj, old_matrix):
+ # Change the parent
+ set_custom_property_value(
+ obj, self.prop_bone, self.prop_id, int(self.selected),
+ keyflags=self.keyflags_switch
+ )
+
+ context.view_layer.update()
+
+ # Set the transforms to restore position
+ set_transform_from_matrix(
+ obj, self.bone, old_matrix, keyflags=self.keyflags,
+ no_loc=self.locks[0], no_rot=self.locks[1], no_scale=self.locks[2]
+ )
+
+ def get_bone_props(self):
+ props = set()
+ if not self.locks[0]:
+ props |= TRANSFORM_PROPS_LOCATION
+ if not self.locks[1]:
+ props |= TRANSFORM_PROPS_ROTATION
+ if not self.locks[2]:
+ props |= TRANSFORM_PROPS_SCALE
+ return props
+
+ def init_invoke(self, context):
+ pose = context.active_object.pose
+
+ if (not pose or not self.parent_names
+ or self.bone not in pose.bones
+ or self.prop_bone not in pose.bones
+ or self.prop_id not in pose.bones[self.prop_bone]):
+ self.report({'ERROR'}, "Invalid parameters")
+ return {'CANCELLED'}
+
+ parents = json.loads(self.parent_names)
+ pitems = [(str(i), name, name) for i, name in enumerate(parents)]
+
+ RigifySwitchParentBase.parent_items = pitems
+
+ self.selected = str(pose.bones[self.prop_bone][self.prop_id])
+
+
+class POSE_OT_rigify_switch_parent(RigifySwitchParentBase, RigifySingleUpdateMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_switch_parent_" + rig_id
+ bl_label = "Switch Parent (Keep Transform)"
+ bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
+ bl_description = "Switch parent, preserving the bone position and orientation"
+
+ def draw(self, _context):
+ col = self.layout.column()
+ col.prop(self, 'selected', expand=True)
+
+
+class POSE_OT_rigify_switch_parent_bake(RigifySwitchParentBase, RigifyBakeKeyframesMixin, bpy.types.Operator):
+ bl_idname = "pose.rigify_switch_parent_bake_" + rig_id
+ bl_label = "Apply Switch Parent To Keyframes"
+ bl_options = {'UNDO', 'INTERNAL'}
+ bl_description = "Switch parent over a frame range, adjusting keys to preserve the bone position and orientation"
+
+ def execute_scan_curves(self, context, obj):
+ return self.bake_add_bone_frames(self.bone, self.get_bone_props())
+
+ def execute_before_apply(self, context, obj, range, range_raw):
+ self.bake_replace_custom_prop_keys_constant(self.prop_bone, self.prop_id, int(self.selected))
+
+ def draw(self, context):
+ self.layout.prop(self, 'selected', text='')
+''']
diff --git a/rigify/utils/widgets_basic.py b/rigify/utils/widgets_basic.py
index aae8f6bb..de04aecc 100644
--- a/rigify/utils/widgets_basic.py
+++ b/rigify/utils/widgets_basic.py
@@ -109,12 +109,12 @@ def create_limb_widget(rig, bone_name, bone_transform_name=None):
mesh.update()
-def create_bone_widget(rig, bone_name, bone_transform_name=None):
+def create_bone_widget(rig, bone_name, r1=0.1, l1=0.0, r2=0.04, l2=1.0, bone_transform_name=None):
""" Creates a basic bone widget, a simple obolisk-esk shape.
"""
obj = create_widget(rig, bone_name, bone_transform_name)
if obj != None:
- verts = [(0.04, 1.0, -0.04), (0.1, 0.0, -0.1), (-0.1, 0.0, -0.1), (-0.04, 1.0, -0.04), (0.04, 1.0, 0.04), (0.1, 0.0, 0.1), (-0.1, 0.0, 0.1), (-0.04, 1.0, 0.04)]
+ verts = [(r2, l2, -r2), (r1, l1, -r1), (-r1, l1, -r1), (-r2, l2, -r2), (r2, l2, r2), (r1, l1, r1), (-r1, l1, r1), (-r2, l2, r2)]
edges = [(1, 2), (0, 1), (0, 3), (2, 3), (4, 5), (5, 6), (6, 7), (4, 7), (1, 5), (0, 4), (2, 6), (3, 7)]
mesh = obj.data
mesh.from_pydata(verts, edges, [])