# SPDX-License-Identifier: GPL-2.0-or-later import json from .naming import strip_prefix, make_derived_name from .bones import set_bone_orientation from .mechanism import MechanismUtilityMixin from .rig import rig_is_child from .misc import OptionalLazy, force_lazy, Lazy from ..base_rig import BaseRig from ..base_generate import GeneratorPlugin, BaseGenerator from typing import Optional, Any from collections import defaultdict from itertools import chain from mathutils import Matrix class SwitchParentBuilder(GeneratorPlugin, MechanismUtilityMixin): """ Implements centralized generation of switchable parent mechanisms. Allows all rigs to register their bones as possible parents for other rigs. """ global_parents: list[dict[str, Any]] local_parents: dict[int, list[dict[str, Any]]] parent_list: list[dict[str, Any]] child_list: list[dict[str, Any]] child_map: dict[str, dict[str, Any]] def __init__(self, generator: BaseGenerator): super().__init__(generator) self.child_list = [] self.global_parents = [] self.local_parents = defaultdict(list) self.child_map = {} self.frozen = False self.register_parent(None, 'root', name='Root', is_global=True) ############################## # API def register_parent(self, rig: Optional[BaseRig], bone: Lazy[str], *, name: Optional[str] = None, is_global=False, exclude_self=False, inject_into: Optional[BaseRig] = None, tags: Optional[set[str]] = None): """ Registers a bone of the specified rig as a possible parent. Parameters: rig: Owner of the bone (can be None if is_global). 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. inject_into: Make this parent available to children of the specified rig. tags: Set of tags to use for default parent selection. 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 is_global or rig assert isinstance(bone, str) or callable(bone) assert callable(bone) or rig_is_child(rig, self.generator.bone_owners[bone]) assert rig_is_child(rig, inject_into) real_rig = rig if inject_into and inject_into is not rig: rig = inject_into tags = (tags or set()) | {'injected'} entry = { 'rig': rig, 'bone': bone, 'name': name, 'tags': tags, 'is_global': is_global, 'exclude_self': exclude_self, 'real_rig': real_rig, 'used': False, } if is_global: self.global_parents.append(entry) else: self.local_parents[id(rig)].append(entry) def build_child(self, rig: BaseRig, bone: str, *, use_parent_mch: bool = True, mch_orientation: Optional[str | Matrix] = None, # Options below must be in child_option_table and can be used in amend_child extra_parents: OptionalLazy[list[str | tuple[str, str]]] = None, select_parent: OptionalLazy[str] = None, select_tags: OptionalLazy[list[str | set[str]]] = None, ignore_global: bool = False, exclude_self: bool = False, allow_self: bool = False, context_rig: Optional[BaseRig] = None, no_implicit: bool = False, only_selected: bool = False, prop_bone: OptionalLazy[str] = None, prop_id: Optional[str] = None, prop_name: Optional[str] = None, controls: OptionalLazy[list[str]] = None, ctrl_bone: Optional[str] = None, no_fix_location: bool = False, no_fix_rotation: bool = False, no_fix_scale: bool = False, copy_location: OptionalLazy[str] = None, copy_rotation: OptionalLazy[str] = None, copy_scale: OptionalLazy[str] = None, inherit_scale: str = 'AVERAGE'): """ 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. mch_orientation: Orientation matrix or bone name to align the MCH bone to; defaults to world. select_parent: Select the specified bone instead of the last one. select_tags: List of parent tags to try for default selection. ignore_global: Ignore the is_global flag of potential parents. exclude_self: Ignore parents registered by the rig itself. allow_self: Ignore the 'exclude_self' setting of the parent. context_rig: Rig to use for selecting parents; defaults to rig. no_implicit: Only use parents listed as extra_parents. only_selected: Like no_implicit, but allow the 'default' selected parent. 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_location: Disable "Switch and Keep Transform" correction for location. no_fix_rotation: Disable "Switch and Keep Transform" correction for rotation. no_fix_scale: Disable "Switch and Keep Transform" correction for scale. copy_location: Override the location by copying from another bone. copy_rotation: Override the rotation by copying from another bone. copy_scale: Override the scale by copying from another bone. inherit_scale: Inherit scale mode for the child bone (default: AVERAGE). 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) set_bone_orientation(rig.obj, mch_bone, mch_orientation or Matrix.Identity(4)) else: mch_bone = bone child = { 'rig': rig, 'bone': bone, 'mch_bone': mch_bone, 'is_done': False, 'is_configured': False, } self.assign_child_options(child, self.child_option_table, locals()) self.child_list.append(child) self.child_map[bone] = child def amend_child(self, rig: BaseRig, bone: str, **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, set(options.keys()), options) def rig_child_now(self, bone: str): """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', 'prop_bone', 'prop_id', 'prop_name', 'controls', 'select_parent', 'ignore_global', 'exclude_self', 'allow_self', 'context_rig', 'select_tags', 'no_implicit', 'only_selected', 'ctrl_bone', 'no_fix_location', 'no_fix_rotation', 'no_fix_scale', 'copy_location', 'copy_rotation', 'copy_scale', 'inherit_scale', } def assign_child_options(self, child, names: set[str], options: dict[str, Any]): if 'context_rig' in names: assert rig_is_child(child['rig'], options['context_rig']) for name in names: if name not in self.child_option_table: raise AttributeError('invalid child option: ' + name) child[name] = options[name] def get_rig_parent_candidates(self, rig: Optional[BaseRig]): candidates = [] # Build a list in parent hierarchy order while rig: candidates.append(self.local_parents[id(rig)]) rig = rig.rigify_parent candidates.append(self.global_parents) return list(chain.from_iterable(reversed(candidates))) def generate_bones(self): self.frozen = True self.parent_list = (self.global_parents + list(chain.from_iterable(self.local_parents.values()))) # Link children to parents for child in self.child_list: child_rig = child['context_rig'] or child['rig'] parents = [] for parent in self.get_rig_parent_candidates(child_rig): parent_rig = parent['rig'] # Exclude injected parents if parent['real_rig'] is not parent_rig: if rig_is_child(parent_rig, child_rig): continue if parent['rig'] is child_rig: if (parent['exclude_self'] and not child['allow_self'])\ or child['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'] = force_lazy(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, inherit_scale=child['inherit_scale']) 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() parent_tags = defaultdict(set) for parent in child['parents']: if parent['bone'] not in parent_map: parent_map[parent['bone']] = parent['name'] if parent['tags']: parent_tags[parent['bone']] |= parent['tags'] last_main_parent_bone = child['parents'][-1]['bone'] extra_parents = set() for parent in force_lazy(child['extra_parents'] or []): if not isinstance(parent, tuple): parent = (parent, None) extra_parents.add(parent[0]) if parent[0] not in parent_map: parent_map[parent[0]] = parent[1] for parent in parent_map: if parent in self.child_map: parent_tags[parent] |= {'child'} parent_bones = list(parent_map.items()) # Find which bone to select select_bone = force_lazy(child['select_parent']) or last_main_parent_bone select_tags = force_lazy(child['select_tags']) or [] if child['no_implicit']: assert len(extra_parents) > 0 parent_bones = [item for item in parent_bones if item[0] in extra_parents] if last_main_parent_bone not in extra_parents: last_main_parent_bone = parent_bones[-1][0] for tag in select_tags: tag_set = tag if isinstance(tag, set) else {tag} matching = [ bone for (bone, _) in parent_bones if not tag_set.isdisjoint(parent_tags[bone]) ] if len(matching) > 0: select_bone = matching[-1] break if select_bone not in parent_map: print(f"RIGIFY ERROR: Can't find bone '{select_bone}' " f"to select as default parent of '{bone}'\n") select_bone = last_main_parent_bone if child['only_selected']: filter_set = {select_bone, *extra_parents} parent_bones = [item for item in parent_bones if item[0] in filter_set] try: select_index = 1 + next(i for i, (bone, _) in enumerate(parent_bones) if bone == select_bone) except StopIteration: select_index = len(parent_bones) print("RIGIFY ERROR: Invalid default parent '%s' of '%s'\n" % (select_bone, bone)) child['parent_bones'] = parent_bones # Create the controlling property prop_bone = child['prop_bone'] = force_lazy(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'] = [force_lazy(child[n]) for n in ['copy_location', 'copy_rotation', 'copy_scale']] locks = tuple(bool(n_fix or copy) for n_fix, copy in zip(no_fix, child['copy'])) # Create the script for the property controls = force_lazy(child['controls']) or {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) left_split = row.split(factor=0.75, align=True) # noinspection SpellCheckingInspection left_split.operator('pose.rigify_switch_parent_{rig_id}', text=prop_name, icon='DOWNARROW_HLT', properties=op_props) left_split.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', targets=[(parent, 0.0) for parent, _ in child['parent_bones']] ) prop_var = [(child['prop_bone'], child['prop_id'])] for i, (_parent, _parent_name) in enumerate(child['parent_bones']): expr = 'var == %d' % (i+1) self.make_driver(con.targets[i], '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'] # noinspection SpellCheckingInspection 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 ) 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 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_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, transform_props_with_locks(*self.locks)) 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='') ''']