blender-addons/rigify/utils/misc.py

381 lines
10 KiB
Python

# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
import math
import collections
import typing
from abc import ABC
from itertools import tee, chain, islice, repeat, permutations
from mathutils import Vector, Matrix, Color
from rna_prop_ui import rna_idprop_value_to_python
T = typing.TypeVar('T')
IdType = typing.TypeVar('IdType', bound=bpy.types.ID)
AnyVector = Vector | typing.Sequence[float]
##############################################
# Math
##############################################
axis_vectors = {
'x': (1, 0, 0),
'y': (0, 1, 0),
'z': (0, 0, 1),
'-x': (-1, 0, 0),
'-y': (0, -1, 0),
'-z': (0, 0, -1),
}
# Matrices that reshuffle axis order and/or invert them
shuffle_matrix = {
sx+x+sy+y+sz+z: Matrix((
axis_vectors[sx+x], axis_vectors[sy+y], axis_vectors[sz+z]
)).transposed().freeze()
for x, y, z in permutations(['x', 'y', 'z'])
for sx in ('', '-')
for sy in ('', '-')
for sz in ('', '-')
}
def angle_on_plane(plane: Vector, vec1: Vector, vec2: Vector):
""" Return the angle between two vectors projected onto a plane.
"""
plane.normalize()
vec1 = vec1 - (plane * (vec1.dot(plane)))
vec2 = vec2 - (plane * (vec2.dot(plane)))
vec1.normalize()
vec2.normalize()
# Determine the angle
angle = math.acos(max(-1.0, min(1.0, vec1.dot(vec2))))
if angle < 0.00001: # close enough to zero that sign doesn't matter
return angle
# Determine the sign of the angle
vec3 = vec2.cross(vec1)
vec3.normalize()
sign = vec3.dot(plane)
if sign >= 0:
sign = 1
else:
sign = -1
return angle * sign
# Convert between a matrix and axis+roll representations.
# Re-export the C implementation internally used by bones.
matrix_from_axis_roll = bpy.types.Bone.MatrixFromAxisRoll
axis_roll_from_matrix = bpy.types.Bone.AxisRollFromMatrix
def matrix_from_axis_pair(y_axis: AnyVector, other_axis: AnyVector, axis_name: str):
assert axis_name in 'xz'
y_axis = Vector(y_axis).normalized()
if axis_name == 'x':
z_axis = Vector(other_axis).cross(y_axis).normalized()
x_axis = y_axis.cross(z_axis)
else:
x_axis = y_axis.cross(other_axis).normalized()
z_axis = x_axis.cross(y_axis)
return Matrix((x_axis, y_axis, z_axis)).transposed()
##############################################
# Color correction functions
##############################################
# noinspection SpellCheckingInspection
def linsrgb_to_srgb(linsrgb: float):
"""Convert physically linear RGB values into sRGB ones. The transform is
uniform in the components, so *linsrgb* can be of any shape.
*linsrgb* values should range between 0 and 1, inclusively.
"""
# From Wikipedia, but easy analogue to the above.
gamma = 1.055 * linsrgb**(1./2.4) - 0.055
scale = linsrgb * 12.92
# return np.where (linsrgb > 0.0031308, gamma, scale)
if linsrgb > 0.0031308:
return gamma
return scale
def gamma_correct(color: Color):
corrected_color = Color()
for i, component in enumerate(color): # noqa
corrected_color[i] = linsrgb_to_srgb(color[i]) # noqa
return corrected_color
##############################################
# Iterators
##############################################
# noinspection SpellCheckingInspection
def padnone(iterable, pad=None):
return chain(iterable, repeat(pad))
# noinspection SpellCheckingInspection
def pairwise_nozip(iterable):
"""s -> (s0,s1), (s1,s2), (s2,s3), ..."""
a, b = tee(iterable)
next(b, None)
return a, b
def pairwise(iterable):
"""s -> (s0,s1), (s1,s2), (s2,s3), ..."""
a, b = tee(iterable)
next(b, None)
return zip(a, b)
def map_list(func, *inputs):
"""[func(a0,b0...), func(a1,b1...), ...]"""
return list(map(func, *inputs))
def skip(n, iterable):
"""Returns an iterator skipping first n elements of an iterable."""
iterator = iter(iterable)
if n == 1:
next(iterator, None)
else:
next(islice(iterator, n, n), None)
return iterator
def map_apply(func, *inputs):
"""Apply the function to inputs like map for side effects, discarding results."""
collections.deque(map(func, *inputs), maxlen=0)
def find_index(sequence, item, default=None):
for i, elem in enumerate(sequence):
if elem == item:
return i
return default
##############################################
# Lazy references
##############################################
Lazy: typing.TypeAlias = T | typing.Callable[[], T]
OptionalLazy: typing.TypeAlias = typing.Optional[T | typing.Callable[[], T]]
def force_lazy(value: OptionalLazy[T]) -> T:
"""If the argument is callable, invokes it without arguments.
Otherwise, returns the argument as is."""
if callable(value):
return value()
else:
return value
class LazyRef(typing.Generic[T]):
"""Hashable lazy reference. When called, evaluates (foo, 'a', 'b'...) as foo('a','b')
if foo is callable. Otherwise, the remaining arguments are used as attribute names or
keys, like foo.a.b or foo.a[b] etc."""
def __init__(self, first, *args):
self.first = first
self.args = tuple(args)
self.first_hashable = first.__hash__ is not None
def __repr__(self):
return 'LazyRef{}'.format((self.first, *self.args))
def __eq__(self, other):
return (
isinstance(other, LazyRef) and
(self.first == other.first if self.first_hashable else self.first is other.first) and
self.args == other.args
)
def __hash__(self):
return (hash(self.first) if self.first_hashable
else hash(id(self.first))) ^ hash(self.args)
def __call__(self) -> T:
first = self.first
if callable(first):
return first(*self.args)
for item in self.args:
if isinstance(first, (dict, list)):
first = first[item]
else:
first = getattr(first, item)
return first
##############################################
# Misc
##############################################
def copy_attributes(a, b):
keys = dir(a)
for key in keys:
if not (key.startswith("_") or
key.startswith("error_") or
key in ("group", "is_valid", "is_valid", "bl_rna")):
try:
setattr(b, key, getattr(a, key))
except AttributeError:
pass
def property_to_python(value) -> typing.Any:
value = rna_idprop_value_to_python(value)
if isinstance(value, dict):
return {k: property_to_python(v) for k, v in value.items()}
elif isinstance(value, list):
return map_list(property_to_python, value)
else:
return value
def clone_parameters(target):
return property_to_python(dict(target))
def assign_parameters(target, val_dict=None, **params):
if val_dict is not None:
for key in list(target.keys()):
del target[key]
data = {**val_dict, **params}
else:
data = params
for key, value in data.items():
try:
target[key] = value
except Exception as e:
raise Exception(f"Couldn't set {key} to {value}: {e}")
def select_object(context: bpy.types.Context, obj: bpy.types.Object, deselect_all=False):
view_layer = context.view_layer
if deselect_all:
for layer_obj in view_layer.objects:
layer_obj.select_set(False) # deselect all objects
obj.select_set(True)
view_layer.objects.active = obj
def choose_next_uid(collection: typing.Iterable, prop_name: str, *, min_value=0):
return 1 + max(
(getattr(obj, prop_name, min_value - 1) for obj in collection),
default=min_value-1,
)
##############################################
# Text
##############################################
def wrap_list_to_lines(prefix: str, delimiters: tuple[str, str] | str,
items: typing.Iterable[str], *,
limit=90, indent=4) -> list[str]:
"""
Generate a string representation of a list of items, wrapping lines if necessary.
Args:
prefix: Text of the first line before the list.
delimiters: Start and end of list delimiters.
items: List items, already converted to strings.
limit: Maximum line length.
indent: Wrapped line indent relative to prefix.
"""
start, end = delimiters
items = list(items)
simple_line = prefix + start + ', '.join(items) + end
if not items or len(simple_line) <= limit:
return [simple_line]
prefix_indent = prefix[0: len(prefix) - len(prefix.lstrip())]
inner_indent = prefix_indent + ' ' * indent
result = []
line = prefix + start
for item in items:
item_repr = item + ','
if not result or len(line) + len(item_repr) + 1 > limit:
result.append(line)
line = inner_indent + item_repr
else:
line += ' ' + item_repr
result.append(line[:-1] + end)
return result
##############################################
# Typing
##############################################
class TypedObject(bpy.types.Object, typing.Generic[IdType]):
data: IdType
ArmatureObject = TypedObject[bpy.types.Armature]
MeshObject = TypedObject[bpy.types.Mesh]
def verify_armature_obj(obj: bpy.types.Object) -> ArmatureObject:
assert obj and obj.type == 'ARMATURE'
return obj # noqa
def verify_mesh_obj(obj: bpy.types.Object) -> MeshObject:
assert obj and obj.type == 'MESH'
return obj # noqa
class IdPropSequence(typing.Mapping[str, T], typing.Sequence[T], ABC):
def __getitem__(self, item: str | int) -> T:
pass
def __setitem__(self, key: str | int, value: T):
pass
def __iter__(self) -> typing.Iterator[T]:
pass
def add(self) -> T:
pass
def clear(self):
pass
def move(self, from_idx: int, to_idx: int):
pass
def remove(self, item: int):
pass