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/rig_ui_template.py
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/rig_ui_template.py')
-rw-r--r--rigify/rig_ui_template.py565
1 files changed, 459 insertions, 106 deletions
diff --git a/rigify/rig_ui_template.py b/rigify/rig_ui_template.py
index 6180ad03..8c66b8b3 100644
--- a/rigify/rig_ui_template.py
+++ b/rigify/rig_ui_template.py
@@ -18,12 +18,28 @@
# <pep8 compliant>
+import bpy
+
+from collections import OrderedDict
+
+from .utils.animation import SCRIPT_REGISTER_BAKE, SCRIPT_UTILITIES_BAKE
+from .utils.layers import get_layers
+from .utils.rig import attach_persistent_script
+
+from . import base_generate
+
+from rna_prop_ui import rna_idprop_quote_path
+
+
UI_IMPORTS = [
'import bpy',
- 'from bpy.props import StringProperty',
'import math',
+ 'import json',
+ 'import collections',
'from math import pi',
+ 'from bpy.props import StringProperty',
'from mathutils import Euler, Matrix, Quaternion, Vector',
+ 'from rna_prop_ui import rna_idprop_quote_path',
]
UI_BASE_UTILITIES = '''
@@ -63,42 +79,21 @@ def rotation_difference(mat1, mat2):
angle = -angle + (2*pi)
return angle
-def tail_distance(angle,bone_ik,bone_fk):
- """ Returns the distance between the tails of two bones
- after rotating bone_ik in AXIS_ANGLE mode.
- """
- rot_mod=bone_ik.rotation_mode
- if rot_mod != 'AXIS_ANGLE':
- bone_ik.rotation_mode = 'AXIS_ANGLE'
- bone_ik.rotation_axis_angle[0] = angle
- bpy.context.view_layer.update()
-
- dv = (bone_fk.tail - bone_ik.tail).length
-
- bone_ik.rotation_mode = rot_mod
- return dv
-
-def find_min_range(bone_ik,bone_fk,f=tail_distance,delta=pi/8):
+def find_min_range(f,start_angle,delta=pi/8):
""" finds the range where lies the minimum of function f applied on bone_ik and bone_fk
at a certain angle.
"""
- rot_mod=bone_ik.rotation_mode
- if rot_mod != 'AXIS_ANGLE':
- bone_ik.rotation_mode = 'AXIS_ANGLE'
-
- start_angle = bone_ik.rotation_axis_angle[0]
angle = start_angle
while (angle > (start_angle - 2*pi)) and (angle < (start_angle + 2*pi)):
- l_dist = f(angle-delta,bone_ik,bone_fk)
- c_dist = f(angle,bone_ik,bone_fk)
- r_dist = f(angle+delta,bone_ik,bone_fk)
+ l_dist = f(angle-delta)
+ c_dist = f(angle)
+ r_dist = f(angle+delta)
if min((l_dist,c_dist,r_dist)) == c_dist:
- bone_ik.rotation_mode = rot_mod
return (angle-delta,angle+delta)
else:
angle=angle+delta
-def ternarySearch(f, left, right, bone_ik, bone_fk, absolutePrecision):
+def ternarySearch(f, left, right, absolutePrecision):
"""
Find minimum of unimodal function f() within [left, right]
To find the maximum, revert the if/else statement or revert the comparison.
@@ -111,11 +106,13 @@ def ternarySearch(f, left, right, bone_ik, bone_fk, absolutePrecision):
leftThird = left + (right - left)/3
rightThird = right - (right - left)/3
- if f(leftThird, bone_ik, bone_fk) > f(rightThird, bone_ik, bone_fk):
+ if f(leftThird) > f(rightThird):
left = leftThird
else:
right = rightThird
+'''
+UTILITIES_FUNC_COMMON_IKFK = ['''
#########################################
## "Visual Transform" helper functions ##
#########################################
@@ -125,28 +122,8 @@ def get_pose_matrix_in_other_space(mat, pose_bone):
transform space. In other words, presuming that mat is in
armature space, slapping the returned matrix onto pose_bone
should give it the armature-space transforms of mat.
- TODO: try to handle cases with axis-scaled parents better.
"""
- rest = pose_bone.bone.matrix_local.copy()
- rest_inv = rest.inverted()
- if pose_bone.parent:
- par_mat = pose_bone.parent.matrix.copy()
- par_inv = par_mat.inverted()
- par_rest = pose_bone.parent.bone.matrix_local.copy()
- else:
- par_mat = Matrix()
- par_inv = Matrix()
- par_rest = Matrix()
-
- # Get matrix in bone's current transform space
- smat = rest_inv @ (par_rest @ (par_inv @ mat))
-
- # Compensate for non-local location
- #if not pose_bone.bone.use_local_location:
- # loc = smat.to_translation() @ (par_rest.inverted() @ rest).to_quaternion()
- # smat.translation = loc
-
- return smat
+ return pose_bone.id_data.convert_space(matrix=mat, pose_bone=pose_bone, from_space='POSE', to_space='LOCAL')
def get_local_pose_matrix(pose_bone):
@@ -159,19 +136,7 @@ def set_pose_translation(pose_bone, mat):
""" Sets the pose bone's translation to the same translation as the given matrix.
Matrix should be given in bone's local space.
"""
- if pose_bone.bone.use_local_location == True:
- pose_bone.location = mat.to_translation()
- else:
- loc = mat.to_translation()
-
- rest = pose_bone.bone.matrix_local.copy()
- if pose_bone.bone.parent:
- par_rest = pose_bone.bone.parent.matrix_local.copy()
- else:
- par_rest = Matrix()
-
- q = (par_rest.inverted() @ rest).to_quaternion()
- pose_bone.location = q @ loc
+ pose_bone.location = mat.to_translation()
def set_pose_rotation(pose_bone, mat):
@@ -205,8 +170,6 @@ def match_pose_translation(pose_bone, target_bone):
"""
mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone)
set_pose_translation(pose_bone, mat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
def match_pose_rotation(pose_bone, target_bone):
@@ -216,8 +179,6 @@ def match_pose_rotation(pose_bone, target_bone):
"""
mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone)
set_pose_rotation(pose_bone, mat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
def match_pose_scale(pose_bone, target_bone):
@@ -227,27 +188,55 @@ def match_pose_scale(pose_bone, target_bone):
"""
mat = get_pose_matrix_in_other_space(target_bone.matrix, pose_bone)
set_pose_scale(pose_bone, mat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
-def correct_rotation(bone_ik, bone_fk):
+
+##############################
+## IK/FK snapping functions ##
+##############################
+
+def correct_rotation(view_layer, bone_ik, target_matrix):
""" Corrects the ik rotation in ik2fk snapping functions
"""
- alfarange = find_min_range(bone_ik,bone_fk)
- alfamin = ternarySearch(tail_distance,alfarange[0],alfarange[1],bone_ik,bone_fk,0.1)
+ axis = target_matrix.to_3x3().col[1].normalized()
- rot_mod = bone_ik.rotation_mode
- if rot_mod != 'AXIS_ANGLE':
- bone_ik.rotation_mode = 'AXIS_ANGLE'
- bone_ik.rotation_axis_angle[0] = alfamin
- bone_ik.rotation_mode = rot_mod
+ def distance(angle):
+ # Rotate the bone and return the actual angle between bones
+ bone_ik.rotation_euler[1] = angle
+ view_layer.update()
-##############################
-## IK/FK snapping functions ##
-##############################
+ return -(bone_ik.vector.normalized().dot(axis))
+
+ if bone_ik.rotation_mode in {'QUATERNION', 'AXIS_ANGLE'}:
+ bone_ik.rotation_mode = 'ZXY'
+
+ start_angle = bone_ik.rotation_euler[1]
+
+ alfarange = find_min_range(distance, start_angle)
+ alfamin = ternarySearch(distance, alfarange[0], alfarange[1], pi / 180)
+
+ bone_ik.rotation_euler[1] = alfamin
+ view_layer.update()
+
+
+def correct_scale(view_layer, bone_ik, target_matrix):
+ """ Correct the scale of the base IK bone. """
+ input_scale = target_matrix.to_scale()
-def match_pole_target(ik_first, ik_last, pole, match_bone, length):
+ for i in range(3):
+ cur_scale = bone_ik.matrix.to_scale()
+
+ bone_ik.scale = [
+ v * i / c for v, i, c in zip(bone_ik.scale, input_scale, cur_scale)
+ ]
+
+ view_layer.update()
+
+ if all(abs((c - i)/i) < 0.01 for i, c in zip(input_scale, cur_scale)):
+ break
+
+
+def match_pole_target(view_layer, ik_first, ik_last, pole, match_bone_matrix, length):
""" Places an IK chain's pole target to match ik_first's
transforms to match_bone. All bones should be given as pose bones.
You need to be in pose mode on the relevant armature object.
@@ -278,22 +267,21 @@ def match_pole_target(ik_first, ik_last, pole, match_bone, length):
mat = get_pose_matrix_in_other_space(Matrix.Translation(ploc), pole)
set_pose_translation(pole, mat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
+ view_layer.update()
set_pole(pv)
# Get the rotation difference between ik_first and match_bone
- angle = rotation_difference(ik_first.matrix, match_bone.matrix)
+ angle = rotation_difference(ik_first.matrix, match_bone_matrix)
# Try compensating for the rotation difference in both directions
pv1 = Matrix.Rotation(angle, 4, ikv) @ pv
set_pole(pv1)
- ang1 = rotation_difference(ik_first.matrix, match_bone.matrix)
+ ang1 = rotation_difference(ik_first.matrix, match_bone_matrix)
pv2 = Matrix.Rotation(-angle, 4, ikv) @ pv
set_pole(pv2)
- ang2 = rotation_difference(ik_first.matrix, match_bone.matrix)
+ ang2 = rotation_difference(ik_first.matrix, match_bone_matrix)
# Do the one with the smaller angle
if ang1 < ang2:
@@ -309,7 +297,7 @@ def parse_bone_names(names_string):
else:
return names_string
-'''
+''']
UTILITIES_FUNC_ARM_FKIK = ['''
######################
@@ -322,6 +310,7 @@ def fk2ik_arm(obj, fk, ik):
fk: list of fk bone names
ik: list of ik bone names
"""
+ view_layer = bpy.context.view_layer
uarm = obj.pose.bones[fk[0]]
farm = obj.pose.bones[fk[1]]
hand = obj.pose.bones[fk[2]]
@@ -341,29 +330,35 @@ def fk2ik_arm(obj, fk, ik):
# Upper arm position
match_pose_rotation(uarm, uarmi)
match_pose_scale(uarm, uarmi)
+ view_layer.update()
# Forearm position
match_pose_rotation(farm, farmi)
match_pose_scale(farm, farmi)
+ view_layer.update()
# Hand position
match_pose_rotation(hand, handi)
match_pose_scale(hand, handi)
+ view_layer.update()
else:
# Upper arm position
match_pose_translation(uarm, uarmi)
match_pose_rotation(uarm, uarmi)
match_pose_scale(uarm, uarmi)
+ view_layer.update()
# Forearm position
#match_pose_translation(hand, handi)
match_pose_rotation(farm, farmi)
match_pose_scale(farm, farmi)
+ view_layer.update()
# Hand position
match_pose_translation(hand, handi)
match_pose_rotation(hand, handi)
match_pose_scale(hand, handi)
+ view_layer.update()
def ik2fk_arm(obj, fk, ik):
@@ -372,6 +367,7 @@ def ik2fk_arm(obj, fk, ik):
fk: list of fk bone names
ik: list of ik bone names
"""
+ view_layer = bpy.context.view_layer
uarm = obj.pose.bones[fk[0]]
farm = obj.pose.bones[fk[1]]
hand = obj.pose.bones[fk[2]]
@@ -395,21 +391,29 @@ def ik2fk_arm(obj, fk, ik):
match_pose_translation(handi, hand)
match_pose_rotation(handi, hand)
match_pose_scale(handi, hand)
+ view_layer.update()
+
# Pole target position
- match_pole_target(uarmi, farmi, pole, uarm, (uarmi.length + farmi.length))
+ match_pole_target(view_layer, uarmi, farmi, pole, uarm.matrix, (uarmi.length + farmi.length))
else:
# Hand position
match_pose_translation(handi, hand)
match_pose_rotation(handi, hand)
match_pose_scale(handi, hand)
+ view_layer.update()
# Upper Arm position
match_pose_translation(uarmi, uarm)
- match_pose_rotation(uarmi, uarm)
+ #match_pose_rotation(uarmi, uarm)
+ set_pose_rotation(uarmi, Matrix())
match_pose_scale(uarmi, uarm)
+ view_layer.update()
+
# Rotation Correction
- correct_rotation(uarmi, uarm)
+ correct_rotation(view_layer, uarmi, uarm.matrix)
+
+ correct_scale(view_layer, uarmi, uarm.matrix)
''']
UTILITIES_FUNC_LEG_FKIK = ['''
@@ -423,6 +427,7 @@ def fk2ik_leg(obj, fk, ik):
fk: list of fk bone names
ik: list of ik bone names
"""
+ view_layer = bpy.context.view_layer
thigh = obj.pose.bones[fk[0]]
shin = obj.pose.bones[fk[1]]
foot = obj.pose.bones[fk[2]]
@@ -444,36 +449,38 @@ def fk2ik_leg(obj, fk, ik):
# Thigh position
match_pose_rotation(thigh, thighi)
match_pose_scale(thigh, thighi)
+ view_layer.update()
# Shin position
match_pose_rotation(shin, shini)
match_pose_scale(shin, shini)
+ view_layer.update()
# Foot position
mat = mfoot.bone.matrix_local.inverted() @ foot.bone.matrix_local
footmat = get_pose_matrix_in_other_space(mfooti.matrix, foot) @ mat
set_pose_rotation(foot, footmat)
set_pose_scale(foot, footmat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
+ view_layer.update()
else:
# Thigh position
match_pose_translation(thigh, thighi)
match_pose_rotation(thigh, thighi)
match_pose_scale(thigh, thighi)
+ view_layer.update()
# Shin position
match_pose_rotation(shin, shini)
match_pose_scale(shin, shini)
+ view_layer.update()
# Foot position
mat = mfoot.bone.matrix_local.inverted() @ foot.bone.matrix_local
footmat = get_pose_matrix_in_other_space(mfooti.matrix, foot) @ mat
set_pose_rotation(foot, footmat)
set_pose_scale(foot, footmat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
+ view_layer.update()
def ik2fk_leg(obj, fk, ik):
@@ -482,6 +489,7 @@ def ik2fk_leg(obj, fk, ik):
fk: list of fk bone names
ik: list of ik bone names
"""
+ view_layer = bpy.context.view_layer
thigh = obj.pose.bones[fk[0]]
shin = obj.pose.bones[fk[1]]
mfoot = obj.pose.bones[fk[2]]
@@ -506,6 +514,7 @@ def ik2fk_leg(obj, fk, ik):
# Clear footroll
set_pose_rotation(footroll, Matrix())
+ view_layer.update()
# Foot position
mat = mfooti.bone.matrix_local.inverted() @ footi.bone.matrix_local
@@ -513,16 +522,17 @@ def ik2fk_leg(obj, fk, ik):
set_pose_translation(footi, footmat)
set_pose_rotation(footi, footmat)
set_pose_scale(footi, footmat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
+ view_layer.update()
# Thigh position
match_pose_translation(thighi, thigh)
- match_pose_rotation(thighi, thigh)
+ #match_pose_rotation(thighi, thigh)
+ set_pose_rotation(thighi, Matrix())
match_pose_scale(thighi, thigh)
+ view_layer.update()
# Rotation Correction
- correct_rotation(thighi,thigh)
+ correct_rotation(view_layer, thighi, thigh.matrix)
else:
# Stretch
@@ -532,6 +542,7 @@ def ik2fk_leg(obj, fk, ik):
# Clear footroll
set_pose_rotation(footroll, Matrix())
+ view_layer.update()
# Foot position
mat = mfooti.bone.matrix_local.inverted() @ footi.bone.matrix_local
@@ -539,11 +550,12 @@ def ik2fk_leg(obj, fk, ik):
set_pose_translation(footi, footmat)
set_pose_rotation(footi, footmat)
set_pose_scale(footi, footmat)
- bpy.ops.object.mode_set(mode='OBJECT')
- bpy.ops.object.mode_set(mode='POSE')
+ view_layer.update()
# Pole target position
- match_pole_target(thighi, shini, pole, thigh, (thighi.length + shini.length))
+ match_pole_target(view_layer, thighi, shini, pole, thigh.matrix, (thighi.length + shini.length))
+
+ correct_scale(view_layer, thighi, thigh.matrix)
''']
UTILITIES_FUNC_POLE = ['''
@@ -765,6 +777,7 @@ class Rigify_Rot2PoleSwitch(bpy.types.Operator):
REGISTER_RIG_ARM = REGISTER_OP_ARM_FKIK + REGISTER_OP_POLE
UTILITIES_RIG_ARM = [
+ *UTILITIES_FUNC_COMMON_IKFK,
*UTILITIES_FUNC_ARM_FKIK,
*UTILITIES_FUNC_POLE,
*UTILITIES_OP_ARM_FKIK,
@@ -774,6 +787,7 @@ UTILITIES_RIG_ARM = [
REGISTER_RIG_LEG = REGISTER_OP_LEG_FKIK + REGISTER_OP_POLE
UTILITIES_RIG_LEG = [
+ *UTILITIES_FUNC_COMMON_IKFK,
*UTILITIES_FUNC_LEG_FKIK,
*UTILITIES_FUNC_POLE,
*UTILITIES_OP_LEG_FKIK,
@@ -794,6 +808,7 @@ UI_REGISTER = [
# Include arm and leg utilities for now in case somebody wants to use
# legacy limb rigs, which expect these to be available by default.
UI_UTILITIES = [
+ *UTILITIES_FUNC_COMMON_IKFK,
*UTILITIES_FUNC_ARM_FKIK,
*UTILITIES_FUNC_LEG_FKIK,
*UTILITIES_OP_ARM_FKIK,
@@ -825,24 +840,44 @@ class RigUI(bpy.types.Panel):
layout = self.layout
pose_bones = context.active_object.pose.bones
try:
- selected_bones = [bone.name for bone in context.selected_pose_bones]
- selected_bones += [context.active_pose_bone.name]
+ selected_bones = set(bone.name for bone in context.selected_pose_bones)
+ selected_bones.add(context.active_pose_bone.name)
except (AttributeError, TypeError):
return
def is_selected(names):
# Returns whether any of the named bones are selected.
- if type(names) == list:
- for name in names:
- if name in selected_bones:
- return True
+ if isinstance(names, list) or isinstance(names, set):
+ return not selected_bones.isdisjoint(names)
elif names in selected_bones:
return True
return False
+ num_rig_separators = [-1]
+ def emit_rig_separator():
+ if num_rig_separators[0] >= 0:
+ layout.separator()
+ num_rig_separators[0] += 1
'''
+UI_REGISTER_BAKE_SETTINGS = ['RigBakeSettings']
+
+UI_BAKE_SETTINGS = '''
+class RigBakeSettings(bpy.types.Panel):
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "Rig Bake Settings"
+ bl_idname = "VIEW3D_PT_rig_bake_settings_" + rig_id
+ bl_category = 'Item'
+
+ @classmethod
+ def poll(self, context):
+ return RigUI.poll(context) and find_action(context.active_object) is not None
+
+ def draw(self, context):
+ RigifyBakeKeyframesMixin.draw_common_bake_ui(context, self.layout)
+'''
def layers_ui(layers, layout):
""" Turn a list of booleans + a list of names into a layer UI.
@@ -896,3 +931,321 @@ class RigLayers(bpy.types.Panel):
code += " row.prop(context.active_object.data, 'layers', index=28, toggle=True, text='Root')\n"
return code
+
+
+def quote_parameters(positional, named):
+ """Quote the given positional and named parameters as a code string."""
+ positional_list = [ repr(v) for v in positional ]
+ named_list = [ "%s=%r" % (k, v) for k, v in named.items() ]
+ return ', '.join(positional_list + named_list)
+
+def indent_lines(lines, indent=4):
+ if indent > 0:
+ prefix = ' ' * indent
+ return [ prefix + line for line in lines ]
+ else:
+ return lines
+
+
+class PanelLayout(object):
+ """Utility class that builds code for creating a layout."""
+
+ def __init__(self, parent, index=0):
+ if isinstance(parent, PanelLayout):
+ self.parent = parent
+ self.script = parent.script
+ else:
+ self.parent = None
+ self.script = parent
+
+ self.header = []
+ self.items = []
+ self.indent = 0
+ self.index = index
+ self.layout = self._get_layout_var(index)
+ self.is_empty = True
+
+ @staticmethod
+ def _get_layout_var(index):
+ return 'layout' if index == 0 else 'group' + str(index)
+
+ def clear_empty(self):
+ self.is_empty = False
+
+ if self.parent:
+ self.parent.clear_empty()
+
+ def get_lines(self):
+ lines = []
+
+ for item in self.items:
+ if isinstance(item, PanelLayout):
+ lines += item.get_lines()
+ else:
+ lines.append(item)
+
+ if len(lines) > 0:
+ return self.wrap_lines(lines)
+ else:
+ return []
+
+ def wrap_lines(self, lines):
+ return self.header + indent_lines(lines, self.indent)
+
+ def add_line(self, line):
+ assert isinstance(line, str)
+
+ self.items.append(line)
+
+ if self.is_empty:
+ self.clear_empty()
+
+ def use_bake_settings(self):
+ """This panel contains operators that need the common Bake settings."""
+ self.parent.use_bake_settings()
+
+ def custom_prop(self, bone_name, prop_name, **params):
+ """Add a custom property input field to the panel."""
+ param_str = quote_parameters([ rna_idprop_quote_path(prop_name) ], params)
+ self.add_line(
+ "%s.prop(pose_bones[%r], %s)" % (self.layout, bone_name, param_str)
+ )
+
+ def operator(self, operator_name, *, properties=None, **params):
+ """Add an operator call button to the panel."""
+ name = operator_name.format_map(self.script.format_args)
+ param_str = quote_parameters([ name ], params)
+ call_str = "%s.operator(%s)" % (self.layout, param_str)
+ if properties:
+ self.add_line("props = " + call_str)
+ for k, v in properties.items():
+ self.add_line("props.%s = %r" % (k,v))
+ else:
+ self.add_line(call_str)
+
+ def add_nested_layout(self, name, params):
+ param_str = quote_parameters([], params)
+ sub_panel = PanelLayout(self, self.index + 1)
+ sub_panel.header.append('%s = %s.%s(%s)' % (sub_panel.layout, self.layout, name, param_str))
+ self.items.append(sub_panel)
+ return sub_panel
+
+ def row(self, **params):
+ """Add a nested row layout to the panel."""
+ return self.add_nested_layout('row', params)
+
+ def column(self, **params):
+ """Add a nested column layout to the panel."""
+ return self.add_nested_layout('column', params)
+
+ def split(self, **params):
+ """Add a split layout to the panel."""
+ return self.add_nested_layout('split', params)
+
+
+class BoneSetPanelLayout(PanelLayout):
+ """Panel restricted to a certain set of bones."""
+
+ def __init__(self, rig_panel, bones):
+ assert isinstance(bones, frozenset)
+ super().__init__(rig_panel)
+ self.bones = bones
+ self.show_bake_settings = False
+
+ def clear_empty(self):
+ self.parent.bones |= self.bones
+
+ super().clear_empty()
+
+ def wrap_lines(self, lines):
+ if self.bones != self.parent.bones:
+ header = ["if is_selected(%r):" % (set(self.bones))]
+ return header + indent_lines(lines)
+ else:
+ return lines
+
+ def use_bake_settings(self):
+ self.show_bake_settings = True
+ if not self.script.use_bake_settings:
+ self.script.use_bake_settings = True
+ self.script.add_utilities(SCRIPT_UTILITIES_BAKE)
+ self.script.register_classes(SCRIPT_REGISTER_BAKE)
+
+
+class RigPanelLayout(PanelLayout):
+ """Panel owned by a certain rig."""
+
+ def __init__(self, script, rig):
+ super().__init__(script)
+ self.bones = set()
+ self.subpanels = OrderedDict()
+
+ def wrap_lines(self, lines):
+ header = [ "if is_selected(%r):" % (set(self.bones)) ]
+ prefix = [ "emit_rig_separator()" ]
+ return header + indent_lines(prefix + lines)
+
+ def panel_with_selected_check(self, control_names):
+ selected_set = frozenset(control_names)
+
+ if selected_set in self.subpanels:
+ return self.subpanels[selected_set]
+ else:
+ panel = BoneSetPanelLayout(self, selected_set)
+ self.subpanels[selected_set] = panel
+ self.items.append(panel)
+ return panel
+
+
+class ScriptGenerator(base_generate.GeneratorPlugin):
+ """Generator plugin that builds the python script attached to the rig."""
+
+ priority = -100
+
+ def __init__(self, generator):
+ super().__init__(generator)
+
+ self.ui_scripts = []
+ self.ui_imports = UI_IMPORTS.copy()
+ self.ui_utilities = UI_UTILITIES.copy()
+ self.ui_register = UI_REGISTER.copy()
+ self.ui_register_drivers = []
+ self.ui_register_props = []
+
+ self.ui_rig_panels = OrderedDict()
+
+ self.use_bake_settings = False
+
+ # Structured panel code generation
+ def panel_with_selected_check(self, rig, control_names):
+ """Add a panel section with restricted selection."""
+ rig_key = id(rig)
+
+ if rig_key in self.ui_rig_panels:
+ panel = self.ui_rig_panels[rig_key]
+ else:
+ panel = RigPanelLayout(self, rig)
+ self.ui_rig_panels[rig_key] = panel
+
+ return panel.panel_with_selected_check(control_names)
+
+ # Raw output
+ def add_panel_code(self, str_list):
+ """Add raw code to the panel."""
+ self.ui_scripts += str_list
+
+ def add_imports(self, str_list):
+ self.ui_imports += str_list
+
+ def add_utilities(self, str_list):
+ self.ui_utilities += str_list
+
+ def register_classes(self, str_list):
+ self.ui_register += str_list
+
+ def register_driver_functions(self, str_list):
+ self.ui_register_drivers += str_list
+
+ def register_property(self, name, definition):
+ self.ui_register_props.append((name, definition))
+
+ def initialize(self):
+ self.format_args = {
+ 'rig_id': self.generator.rig_id,
+ }
+
+ def finalize(self):
+ metarig = self.generator.metarig
+ id_store = self.generator.id_store
+ rig_id = self.generator.rig_id
+
+ vis_layers = self.obj.data.layers
+
+ # Ensure the collection of layer names exists
+ for i in range(1 + len(metarig.data.rigify_layers), 29):
+ metarig.data.rigify_layers.add()
+
+ # Create list of layer name/row pairs
+ layer_layout = []
+ for l in metarig.data.rigify_layers:
+ layer_layout += [(l.name, l.row)]
+
+ # Generate the UI script
+ if id_store.rigify_generate_mode == 'overwrite':
+ rig_ui_name = id_store.rigify_rig_ui or 'rig_ui.py'
+ else:
+ rig_ui_name = 'rig_ui.py'
+
+ if id_store.rigify_generate_mode == 'overwrite' and rig_ui_name in bpy.data.texts.keys():
+ script = bpy.data.texts[rig_ui_name]
+ script.clear()
+ else:
+ script = bpy.data.texts.new("rig_ui.py")
+
+ rig_ui_old_name = ""
+ if id_store.rigify_rig_basename:
+ rig_ui_old_name = script.name
+ script.name = id_store.rigify_rig_basename + "_rig_ui.py"
+
+ id_store.rigify_rig_ui = script.name
+
+ for s in OrderedDict.fromkeys(self.ui_imports):
+ script.write(s + "\n")
+
+ script.write(UI_BASE_UTILITIES % rig_id)
+
+ for s in OrderedDict.fromkeys(self.ui_utilities):
+ script.write(s + "\n")
+
+ script.write(UI_SLIDERS)
+
+ for s in self.ui_scripts:
+ script.write("\n " + s.replace("\n", "\n ") + "\n")
+
+ if len(self.ui_scripts) > 0:
+ script.write("\n num_rig_separators[0] = 0\n")
+
+ for panel in self.ui_rig_panels.values():
+ lines = panel.get_lines()
+ if len(lines) > 1:
+ script.write("\n ".join([''] + lines) + "\n")
+
+ if self.use_bake_settings:
+ self.ui_register = UI_REGISTER_BAKE_SETTINGS + self.ui_register
+ script.write(UI_BAKE_SETTINGS)
+
+ script.write(layers_ui(vis_layers, layer_layout))
+
+ script.write("\ndef register():\n")
+
+ ui_register = OrderedDict.fromkeys(self.ui_register)
+ for s in ui_register:
+ script.write(" bpy.utils.register_class("+s+")\n")
+
+ ui_register_drivers = OrderedDict.fromkeys(self.ui_register_drivers)
+ for s in ui_register_drivers:
+ script.write(" bpy.app.driver_namespace['"+s+"'] = "+s+"\n")
+
+ ui_register_props = OrderedDict.fromkeys(self.ui_register_props)
+ for s in ui_register_props:
+ script.write(" bpy.types.%s = %s\n " % (*s,))
+
+ script.write("\ndef unregister():\n")
+
+ for s in ui_register_props:
+ script.write(" del bpy.types.%s\n" % s[0])
+
+ for s in ui_register:
+ script.write(" bpy.utils.unregister_class("+s+")\n")
+
+ for s in ui_register_drivers:
+ script.write(" del bpy.app.driver_namespace['"+s+"']\n")
+
+ script.write("\nregister()\n")
+ script.use_module = True
+
+ # Run UI script
+ exec(script.as_string(), {})
+
+ # Attach the script to the rig
+ attach_persistent_script(self.obj, script)