488 lines
18 KiB
Python
488 lines
18 KiB
Python
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
||
|
|
|
||
|
|
import bpy
|
||
|
|
|
||
|
|
import re
|
||
|
|
import itertools
|
||
|
|
import bisect
|
||
|
|
import json
|
||
|
|
|
||
|
|
from .errors import MetarigError
|
||
|
|
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 map_list, map_apply, force_lazy
|
||
|
|
|
||
|
|
from ..base_rig import *
|
||
|
|
from ..base_generate import GeneratorPlugin
|
||
|
|
|
||
|
|
from collections import defaultdict
|
||
|
|
from itertools import count, repeat, 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.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, generator):
|
||
|
|
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, bone, *, name=None, is_global=False, exclude_self=False, inject_into=None, tags=None):
|
||
|
|
"""
|
||
|
|
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.
|
||
|
|
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 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, bone, *, use_parent_mch=True, mch_orientation=None, **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.
|
||
|
|
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_* Disable "Switch and Keep Transform" correction for specific channels.
|
||
|
|
copy_* Override the specified components 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 = {
|
||
|
|
**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,
|
||
|
|
'exclude_self': False, 'allow_self': False,
|
||
|
|
'context_rig': None, 'select_tags': None,
|
||
|
|
'no_implicit': False, 'only_selected': False,
|
||
|
|
'ctrl_bone': None,
|
||
|
|
'no_fix_location': False, 'no_fix_rotation': False, 'no_fix_scale': False,
|
||
|
|
'copy_location': None, 'copy_rotation': None, 'copy_scale': None,
|
||
|
|
'inherit_scale': 'AVERAGE',
|
||
|
|
}
|
||
|
|
|
||
|
|
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 get_rig_parent_candidates(self, rig):
|
||
|
|
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("RIGIFY ERROR: Can't find bone '%s' to select as default parent of '%s'\n" % (select_bone, bone))
|
||
|
|
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(nofix or copy) for nofix, copy in zip(no_fix, child['copy']))
|
||
|
|
|
||
|
|
# Create the script for the property
|
||
|
|
controls = force_lazy(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',
|
||
|
|
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']
|
||
|
|
|
||
|
|
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='')
|
||
|
|
''']
|