# SPDX-License-Identifier: GPL-2.0-or-later import bpy import math import collections import typing 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') ############################################## # 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: Vector, other_axis: Vector, 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 # noinspection PyUnresolvedReferences,PyTypeChecker def gamma_correct(color: Color): corrected_color = Color() for i, component in enumerate(color): corrected_color[i] = linsrgb_to_srgb(color[i]) 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) ############################################## # 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: """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): 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 ############################################## # Typing ############################################## class TypedObject(bpy.types.Object, typing.Generic[T]): data: T ArmatureObject = TypedObject[bpy.types.Armature] MeshObject = TypedObject[bpy.types.Mesh] AnyVector = Vector | typing.Sequence[float] def verify_armature_obj(obj: bpy.types.Object) -> ArmatureObject: assert obj and obj.type == 'ARMATURE' # noinspection PyTypeChecker return obj def verify_mesh_obj(obj: bpy.types.Object) -> MeshObject: assert obj and obj.type == 'MESH' # noinspection PyTypeChecker return obj