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:
Diffstat (limited to 'rigify/utils/bones.py')
-rw-r--r--rigify/utils/bones.py332
1 files changed, 277 insertions, 55 deletions
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