Run `make format` to reformat the Rigify code. It now adheres to the global Blender code style standard, rather than having its own style. Most of the changes are simply adding spaces around operators, newlines below docstrings, and changing some indentation. Note that this does not reformat any stored-as-multiline-strings code blocks. No functional changes. Pull Request: https://projects.blender.org/blender/blender/pulls/123833
367 lines
9.7 KiB
Python
367 lines
9.7 KiB
Python
# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import random
|
|
import time
|
|
import re
|
|
import collections
|
|
import enum
|
|
|
|
from typing import Optional, TYPE_CHECKING
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from ..base_generate import BaseGenerator
|
|
|
|
|
|
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_z', 'side', 'number'])
|
|
|
|
|
|
def split_name(name: str):
|
|
name_parts = re.match(
|
|
r'^(?:(ORG|MCH|DEF)-)?(.*?)([._-][tTbB])?([._-][lLrR])?(?:\.(\d+))?$', name)
|
|
return NameParts(*name_parts.groups())
|
|
|
|
|
|
def is_control_bone(name: str):
|
|
return not split_name(name).prefix
|
|
|
|
|
|
def combine_name(parts: NameParts, *, prefix=None, base=None, side_z=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_z if side_z is not None else parts.side_z or '',
|
|
side if side is not None else parts.side or '',
|
|
'.' + eff_number if eff_number else '',
|
|
])
|
|
|
|
|
|
def insert_before_lr(name: str, text: str) -> str:
|
|
parts = split_name(name)
|
|
|
|
if parts.side:
|
|
return combine_name(parts, base=parts.base + text)
|
|
else:
|
|
return name + text
|
|
|
|
|
|
def make_derived_name(name: str, subtype: str, suffix: Optional[str] = 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: NameParts):
|
|
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: NameParts, side: 'Side'):
|
|
if side != Side.MIDDLE:
|
|
side_char = 'L' if side == Side.LEFT else 'R'
|
|
side_str = parts.side or parts.side_z
|
|
|
|
if side_str:
|
|
sep, side_char2 = side_str[0:2]
|
|
if side_char2.lower() == side_char2:
|
|
side_char = side_char.lower()
|
|
else:
|
|
sep = '.'
|
|
|
|
return sep + side_char
|
|
else:
|
|
return ''
|
|
|
|
@staticmethod
|
|
def to_name(parts: NameParts, side: 'Side'):
|
|
new_side = Side.to_string(parts, side)
|
|
return combine_name(parts, side=new_side)
|
|
|
|
|
|
class SideZ(enum.IntEnum):
|
|
TOP = 2
|
|
MIDDLE = 0
|
|
BOTTOM = -2
|
|
|
|
@staticmethod
|
|
def from_parts(parts: NameParts):
|
|
if parts.side_z:
|
|
if parts.side_z[1].lower() == 't':
|
|
return SideZ.TOP
|
|
else:
|
|
return SideZ.BOTTOM
|
|
else:
|
|
return SideZ.MIDDLE
|
|
|
|
@staticmethod
|
|
def to_string(parts: NameParts, side: 'SideZ'):
|
|
if side != SideZ.MIDDLE:
|
|
side_char = 'T' if side == SideZ.TOP else 'B'
|
|
side_str = parts.side_z or parts.side
|
|
|
|
if side_str:
|
|
sep, side_char2 = side_str[0:2]
|
|
if side_char2.lower() == side_char2:
|
|
side_char = side_char.lower()
|
|
else:
|
|
sep = '.'
|
|
|
|
return sep + side_char
|
|
else:
|
|
return ''
|
|
|
|
@staticmethod
|
|
def to_name(parts: NameParts, side: 'SideZ'):
|
|
new_side = SideZ.to_string(parts, side)
|
|
return combine_name(parts, side_z=new_side)
|
|
|
|
|
|
NameSides = collections.namedtuple('NameSides', ['base', 'side', 'side_z'])
|
|
|
|
|
|
def get_name_side(name: str):
|
|
return Side.from_parts(split_name(name))
|
|
|
|
|
|
def get_name_side_z(name: str):
|
|
return SideZ.from_parts(split_name(name))
|
|
|
|
|
|
def get_name_base_and_sides(name: str):
|
|
parts = split_name(name)
|
|
base = combine_name(parts, side='', side_z='')
|
|
return NameSides(base, Side.from_parts(parts), SideZ.from_parts(parts))
|
|
|
|
|
|
def change_name_side(name: str,
|
|
side: Optional[Side] = None, *,
|
|
side_z: Optional[SideZ] = None):
|
|
parts = split_name(name)
|
|
new_side = None if side is None else Side.to_string(parts, side)
|
|
new_side_z = None if side_z is None else SideZ.to_string(parts, side_z)
|
|
return combine_name(parts, side=new_side, side_z=new_side_z)
|
|
|
|
|
|
def mirror_name(name: str):
|
|
parts = split_name(name)
|
|
side = Side.from_parts(parts)
|
|
|
|
if side != Side.MIDDLE:
|
|
return Side.to_name(parts, -side)
|
|
else:
|
|
return name
|
|
|
|
|
|
def mirror_name_z(name: str):
|
|
parts = split_name(name)
|
|
side = SideZ.from_parts(parts)
|
|
|
|
if side != SideZ.MIDDLE:
|
|
return SideZ.to_name(parts, -side)
|
|
else:
|
|
return name
|
|
|
|
|
|
########################################################################
|
|
# Name manipulation
|
|
########################################################################
|
|
|
|
def get_name(bone) -> Optional[str]:
|
|
return bone.name if bone else None
|
|
|
|
|
|
def strip_trailing_number(name: str):
|
|
return combine_name(split_name(name), number='')
|
|
|
|
|
|
def strip_prefix(name: str):
|
|
return combine_name(split_name(name), prefix='')
|
|
|
|
|
|
def unique_name(collection, base_name: str):
|
|
parts = split_name(base_name)
|
|
name = combine_name(parts, number='')
|
|
count = 1
|
|
|
|
while name in collection:
|
|
name = combine_name(parts, number=count)
|
|
count += 1
|
|
|
|
return name
|
|
|
|
|
|
def strip_org(name: str):
|
|
""" Returns the name with ORG_PREFIX stripped from it.
|
|
"""
|
|
if name.startswith(ORG_PREFIX):
|
|
return name[len(ORG_PREFIX):]
|
|
else:
|
|
return name
|
|
|
|
|
|
org_name = strip_org
|
|
|
|
|
|
def strip_mch(name: str):
|
|
""" Returns the name with MCH_PREFIX stripped from it.
|
|
"""
|
|
if name.startswith(MCH_PREFIX):
|
|
return name[len(MCH_PREFIX):]
|
|
else:
|
|
return name
|
|
|
|
|
|
def strip_def(name: str):
|
|
""" Returns the name with DEF_PREFIX stripped from it.
|
|
"""
|
|
if name.startswith(DEF_PREFIX):
|
|
return name[len(DEF_PREFIX):]
|
|
else:
|
|
return name
|
|
|
|
|
|
def org(name: str):
|
|
""" Prepends the ORG_PREFIX to a name if it doesn't already have
|
|
it, and returns it.
|
|
"""
|
|
if name.startswith(ORG_PREFIX):
|
|
return name
|
|
else:
|
|
return ORG_PREFIX + name
|
|
|
|
|
|
make_original_name = org
|
|
|
|
|
|
def mch(name: str):
|
|
""" Prepends the MCH_PREFIX to a name if it doesn't already have
|
|
it, and returns it.
|
|
"""
|
|
if name.startswith(MCH_PREFIX):
|
|
return name
|
|
else:
|
|
return MCH_PREFIX + name
|
|
|
|
|
|
make_mechanism_name = mch
|
|
|
|
|
|
def deformer(name: str):
|
|
""" Prepends the DEF_PREFIX to a name if it doesn't already have
|
|
it, and returns it.
|
|
"""
|
|
if name.startswith(DEF_PREFIX):
|
|
return name
|
|
else:
|
|
return DEF_PREFIX + name
|
|
|
|
|
|
make_deformer_name = deformer
|
|
|
|
|
|
def random_id(length=8):
|
|
""" Generates a random alphanumeric id string.
|
|
"""
|
|
t_length = int(length / 2)
|
|
r_length = int(length / 2) + int(length % 2)
|
|
|
|
chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', 'a', 'b', 'c', 'd', 'e', 'f',
|
|
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
|
|
'x', 'y', 'z']
|
|
text = ""
|
|
for i in range(0, r_length):
|
|
text += random.choice(chars)
|
|
text += str(hex(int(time.time())))[2:][-t_length:].rjust(t_length, '0')[::-1]
|
|
return text
|
|
|
|
|
|
def choose_derived_bone(generator: 'BaseGenerator', original: str, subtype: str, *,
|
|
by_owner=True, recursive=True):
|
|
bones = generator.obj.pose.bones
|
|
names = generator.find_derived_bones(original, by_owner=by_owner, recursive=recursive)
|
|
|
|
direct = make_derived_name(original, subtype)
|
|
if direct in names and direct in bones:
|
|
return direct
|
|
|
|
prefix = _PREFIX_TABLE[subtype] + '-'
|
|
matching = [name for name in names if name.startswith(prefix) and name in bones]
|
|
|
|
if len(matching) > 0:
|
|
return matching[0]
|
|
|
|
# Try matching bones created by legacy rigs just by name - there is no origin data
|
|
from ..base_generate import LegacyRig
|
|
|
|
if isinstance(generator.bone_owners.get(direct), LegacyRig):
|
|
if not by_owner or generator.bone_owners.get(original) is generator.bone_owners[direct]:
|
|
assert direct in bones
|
|
return direct
|
|
|
|
return None
|
|
|
|
|
|
_MIRROR_MAP_RAW = [
|
|
("Left", "Right"),
|
|
("L", "R"),
|
|
]
|
|
_MIRROR_MAP = {
|
|
**{a: b for a, b in _MIRROR_MAP_RAW},
|
|
**{b: a for a, b in _MIRROR_MAP_RAW},
|
|
**{a.lower(): b.lower() for a, b in _MIRROR_MAP_RAW},
|
|
**{b.lower(): a.lower() for a, b in _MIRROR_MAP_RAW},
|
|
}
|
|
_MIRROR_RE = [
|
|
r"(?<![a-z])(left|light)(?![a-z])",
|
|
r"(?<=[\._])(l|r)(?![a-z])",
|
|
]
|
|
|
|
|
|
def mirror_name_fuzzy(name: str) -> str:
|
|
"""Try to mirror a name by trying various patterns without expecting any rigid structure."""
|
|
|
|
for reg in _MIRROR_RE:
|
|
new_name = re.sub(reg, lambda m: _MIRROR_MAP.get(m[0], m[0]), name, flags=re.IGNORECASE)
|
|
if new_name != name:
|
|
return new_name
|
|
|
|
return name
|