Sybren A. Stüvel d1e7346c63 Add-ons: Rigify, reformat code
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
2024-06-28 11:38:31 +02:00

532 lines
17 KiB
Python

# SPDX-FileCopyrightText: 2019-2022 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
import math
import inspect
import functools
from typing import Optional, Callable, TYPE_CHECKING
from bpy.types import Mesh, Object, UILayout, WindowManager
from mathutils import Matrix, Vector, Euler
from itertools import count
from .errors import MetarigError
from .collections import ensure_collection
from .misc import ArmatureObject, MeshObject, AnyVector, verify_mesh_obj, IdPropSequence
from .naming import change_name_side, get_name_side, Side
if TYPE_CHECKING:
from .. import RigifyName
WGT_PREFIX = "WGT-" # Prefix for widget objects
WGT_GROUP_PREFIX = "WGTS_" # noqa; Prefix for the widget collection
##############################################
# Widget creation
##############################################
def obj_to_bone(obj: Object, rig: ArmatureObject, bone_name: str,
bone_transform_name: Optional[str] = None):
""" Places an object at the location/rotation/scale of the given bone.
"""
if bpy.context.mode == 'EDIT_ARMATURE':
raise MetarigError("obj_to_bone(): does not work while in edit mode")
bone = rig.pose.bones[bone_name]
loc = bone.custom_shape_translation
rot = bone.custom_shape_rotation_euler
scale = Vector(bone.custom_shape_scale_xyz)
if bone.use_custom_shape_bone_size:
scale *= bone.length
if bone_transform_name is not None:
bone = rig.pose.bones[bone_transform_name]
elif bone.custom_shape_transform:
bone = bone.custom_shape_transform
shape_mat = Matrix.LocRotScale(loc, Euler(rot), scale)
obj.rotation_mode = 'XYZ'
obj.matrix_basis = rig.matrix_world @ bone.bone.matrix_local @ shape_mat
def create_widget(rig: ArmatureObject, bone_name: str,
bone_transform_name: Optional[str] = None, *,
widget_name: Optional[str] = None,
widget_force_new=False, subsurf=0) -> Optional[MeshObject]:
"""
Creates an empty widget object for a bone, and returns the object.
If the object already existed, returns None.
"""
assert rig.mode != 'EDIT'
from ..base_generate import BaseGenerator
scene = bpy.context.scene
bone = rig.pose.bones[bone_name]
# Access the current generator instance when generating (ugh, globals)
generator = BaseGenerator.instance
if generator:
collection = generator.widget_collection
else:
collection = ensure_collection(bpy.context, WGT_GROUP_PREFIX + rig.name, hidden=True)
use_mirror = generator and generator.use_mirror_widgets
bone_mid_name = change_name_side(bone_name, Side.MIDDLE) if use_mirror else bone_name
obj_name = widget_name or WGT_PREFIX + rig.name + '_' + bone_name
reuse_mesh = None
obj: Optional[MeshObject]
# Check if it already exists in the scene
if not widget_force_new:
obj = None
if generator:
# Check if the widget was already generated
if bone_name in generator.new_widget_table:
return None
# If re-generating, check widgets used by the previous rig
obj = generator.old_widget_table.get(bone_name)
if not obj:
# Search the scene by name
obj = scene.objects.get(obj_name)
if obj and obj.library:
# Second brute force try if the first result is linked
local_objs = [obj for obj in scene.objects
if obj.name == obj_name and not obj.library]
obj = local_objs[0] if local_objs else None
if obj:
# Record the generated widget
if generator:
generator.new_widget_table[bone_name] = obj
# Re-add to the collection if not there for some reason
if obj.name not in collection.objects:
collection.objects.link(obj)
# Flip scale for originally mirrored widgets
if obj.scale.x < 0 < bone.custom_shape_scale_xyz.x:
bone.custom_shape_scale_xyz.x *= -1
# Move object to bone position, in case it changed
obj_to_bone(obj, rig, bone_name, bone_transform_name)
return None
# Create a linked duplicate of the widget assigned in the metarig
reuse_widget = rig.pose.bones[bone_name].custom_shape
if reuse_widget:
subsurf = 0
reuse_mesh = reuse_widget.data
# Create a linked duplicate with the mirror widget
if not reuse_mesh and use_mirror and bone_mid_name != bone_name:
reuse_mesh = generator.widget_mirror_mesh.get(bone_mid_name)
# Create an empty mesh datablock if not linking
if reuse_mesh:
mesh = reuse_mesh
elif use_mirror and bone_mid_name != bone_name:
# When mirroring, untag side from mesh name, and remember it
mesh = bpy.data.meshes.new(change_name_side(obj_name, Side.MIDDLE))
generator.widget_mirror_mesh[bone_mid_name] = mesh
else:
mesh = bpy.data.meshes.new(obj_name)
# Create the object
obj = verify_mesh_obj(bpy.data.objects.new(obj_name, mesh))
collection.objects.link(obj)
# Add the subdivision surface modifier
if subsurf > 0:
mod = obj.modifiers.new("subsurf", 'SUBSURF')
mod.levels = subsurf
# Record the generated widget
if generator:
generator.new_widget_table[bone_name] = obj
# Flip scale for right side if mirroring widgets
if use_mirror and get_name_side(bone_name) == Side.RIGHT:
if bone.custom_shape_scale_xyz.x > 0:
bone.custom_shape_scale_xyz.x *= -1
# Move object to bone position and set layers
obj_to_bone(obj, rig, bone_name, bone_transform_name)
if reuse_mesh:
return None
return obj
##############################################
# Widget choice dropdown
##############################################
_registered_widgets = {}
def _get_valid_args(callback, skip):
spec = inspect.getfullargspec(callback)
return set(spec.args[skip:] + spec.kwonlyargs)
def register_widget(name: str, callback, **default_args):
unwrapped = inspect.unwrap(callback)
if unwrapped != callback:
valid_args = _get_valid_args(unwrapped, 1)
else:
valid_args = _get_valid_args(callback, 2)
_registered_widgets[name] = (callback, valid_args, default_args)
def get_rigify_widgets(id_store: WindowManager) -> IdPropSequence['RigifyName']:
return id_store.rigify_widgets # noqa
def layout_widget_dropdown(layout: UILayout, props, prop_name: str, **kwargs):
"""Create a UI dropdown to select a widget from the known list."""
id_store = bpy.context.window_manager
rigify_widgets = get_rigify_widgets(id_store)
rigify_widgets.clear()
for name in sorted(_registered_widgets):
item = rigify_widgets.add()
item.name = name
layout.prop_search(props, prop_name, id_store, "rigify_widgets", **kwargs)
def create_registered_widget(obj: ArmatureObject, bone_name: str, widget_id: str, **kwargs):
try:
callback, valid_args, default_args = _registered_widgets[widget_id]
except KeyError:
raise MetarigError("Unknown widget name: " + widget_id)
# Convert between radius and size
if kwargs.get('size') and 'size' not in valid_args:
if 'radius' in valid_args and not kwargs.get('radius'):
kwargs['radius'] = kwargs['size'] / 2
elif kwargs.get('radius') and 'radius' not in valid_args:
if 'size' in valid_args and not kwargs.get('size'):
kwargs['size'] = kwargs['radius'] * 2
args = {**default_args, **kwargs}
return callback(obj, bone_name, **{k: v for k, v in args.items() if k in valid_args})
##############################################
# Widget geometry
##############################################
class GeometryData:
verts: list[AnyVector]
edges: list[tuple[int, int]]
faces: list[tuple[int, ...]]
def __init__(self):
self.verts = []
self.edges = []
self.faces = []
def widget_generator(generate_func=None, *, register=None, subsurf=0) -> Callable:
"""
Decorator that encapsulates a call to create_widget, and only requires
the actual function to fill the provided vertex and edge lists.
Accepts parameters of create_widget, plus any keyword arguments the
wrapped function has.
"""
if generate_func is None:
return functools.partial(widget_generator, register=register, subsurf=subsurf)
@functools.wraps(generate_func)
def wrapper(rig: ArmatureObject, bone_name: str, bone_transform_name=None,
widget_name=None, widget_force_new=False, **kwargs):
obj = create_widget(rig, bone_name, bone_transform_name,
widget_name=widget_name, widget_force_new=widget_force_new,
subsurf=subsurf)
if obj is not None:
geom = GeometryData()
generate_func(geom, **kwargs)
mesh: Mesh = obj.data
mesh.from_pydata(geom.verts, geom.edges, geom.faces)
mesh.update()
return obj
else:
return None
if register:
register_widget(register, wrapper)
return wrapper
def generate_lines_geometry(geom: GeometryData,
points: list[AnyVector], *,
matrix: Optional[Matrix] = None, closed_loop=False):
"""
Generates a polyline using given points, optionally closing the loop.
"""
assert len(points) >= 2
base = len(geom.verts)
for i, raw_point in enumerate(points):
point = Vector(raw_point).to_3d()
if matrix:
point = matrix @ point
geom.verts.append(point)
if i > 0:
geom.edges.append((base + i - 1, base + i))
if closed_loop:
geom.edges.append((len(geom.verts) - 1, base))
def generate_circle_geometry(geom: GeometryData, center: AnyVector, radius: float, *,
matrix: Optional[Matrix] = None,
angle_range: Optional[tuple[float, float]] = None,
steps=24, radius_x: Optional[float] = None, depth_x=0):
"""
Generates a circle, adding vertices and edges to the lists.
center, radius: parameters of the circle
matrix: transformation matrix (by default the circle is in the XY plane)
angle_range: a pair of angles to generate an arc of the circle
steps: number of edges to cover the whole circle (reduced for arcs)
"""
assert steps >= 3
start = 0
delta = math.pi * 2 / steps
if angle_range:
start, end = angle_range
if start == end:
steps = 1
else:
steps = max(3, math.ceil(abs(end - start) / delta) + 1)
delta = (end - start) / (steps - 1)
if radius_x is None:
radius_x = radius
center = Vector(center).to_3d() # allow 2d center
points = []
for i in range(steps):
angle = start + delta * i
x = math.cos(angle)
y = math.sin(angle)
points.append(center + Vector((x * radius_x, y * radius, x * x * depth_x)))
generate_lines_geometry(geom, points, matrix=matrix, closed_loop=not angle_range)
def generate_circle_hull_geometry(geom: GeometryData, points: list[AnyVector],
radius: float, gap: float, *,
matrix: Optional[Matrix] = None, steps=24):
"""
Given a list of 2D points forming a convex hull, generate a contour around
it, with each point being circumscribed with a circle arc of given radius,
and keeping the given distance gap from the lines connecting the circles.
"""
assert radius >= gap
if len(points) <= 1:
if points:
generate_circle_geometry(
geom, points[0], radius,
matrix=matrix, steps=steps
)
return
base = len(geom.verts)
points_ex = [points[-1], *points, points[0]]
angle_gap = math.asin(gap / radius)
for i, pt_prev, pt_cur, pt_next in zip(count(0), points_ex[0:], points_ex[1:], points_ex[2:]):
vec_prev = pt_prev - pt_cur
vec_next = pt_next - pt_cur
# Compute bearings to adjacent points
angle_prev = math.atan2(vec_prev.y, vec_prev.x)
angle_next = math.atan2(vec_next.y, vec_next.x)
if angle_next <= angle_prev:
angle_next += math.pi * 2
# Adjust gap for circles that are too close
angle_prev += max(angle_gap, math.acos(min(1, vec_prev.length / radius / 2)))
angle_next -= max(angle_gap, math.acos(min(1, vec_next.length / radius / 2)))
if angle_next > angle_prev:
if len(geom.verts) > base:
geom.edges.append((len(geom.verts) - 1, len(geom.verts)))
generate_circle_geometry(
geom, pt_cur, radius, angle_range=(angle_prev, angle_next),
matrix=matrix, steps=steps
)
if len(geom.verts) > base:
geom.edges.append((len(geom.verts) - 1, base))
def create_circle_polygon(number_verts: int, axis: str, radius=1.0, head_tail=0.0):
""" Creates a basic circle around of an axis selected.
number_verts: number of vertices of the polygon
axis: axis normal to the circle
radius: the radius of the circle
head_tail: where along the length of the bone the circle is (0.0=head, 1.0=tail)
"""
verts = []
edges = []
angle = 2 * math.pi / number_verts
i = 0
assert(axis in 'XYZ')
while i < number_verts:
a = math.cos(i * angle)
b = math.sin(i * angle)
if axis == 'X':
verts.append((head_tail, a * radius, b * radius))
elif axis == 'Y':
verts.append((a * radius, head_tail, b * radius))
elif axis == 'Z':
verts.append((a * radius, b * radius, head_tail))
if i < (number_verts - 1):
edges.append((i, i + 1))
i += 1
edges.append((0, number_verts - 1))
return verts, edges
##############################################
# Widget transformation
##############################################
def adjust_widget_axis(obj: Object, axis='y', offset=0.0):
mesh = obj.data
assert isinstance(mesh, Mesh)
if axis[0] == '-':
s = -1.0
axis = axis[1]
else:
s = 1.0
trans_matrix = Matrix.Translation((0.0, offset, 0.0))
rot_matrix = Matrix.Diagonal((1.0, s, 1.0, 1.0))
if axis == "x":
rot_matrix = Matrix.Rotation(-s * math.pi / 2, 4, 'Z')
trans_matrix = Matrix.Translation((offset, 0.0, 0.0))
elif axis == "z":
rot_matrix = Matrix.Rotation(s * math.pi / 2, 4, 'X')
trans_matrix = Matrix.Translation((0.0, 0.0, offset))
matrix = trans_matrix @ rot_matrix
for vert in mesh.vertices:
vert.co = matrix @ vert.co
def adjust_widget_transform_mesh(obj: Optional[Object], matrix: Matrix,
local: bool | None = None):
"""Adjust the generated widget by applying a correction matrix to the mesh.
If local is false, the matrix is in world space.
If local is True, it's in the local space of the widget.
If local is a bone, it's in the local space of the bone.
"""
if obj:
mesh = obj.data
assert isinstance(mesh, Mesh)
if local is not True:
if local:
assert isinstance(local, bpy.types.PoseBone)
bone_mat = local.id_data.matrix_world @ local.bone.matrix_local
matrix = bone_mat @ matrix @ bone_mat.inverted()
obj_mat = obj.matrix_basis
matrix = obj_mat.inverted() @ matrix @ obj_mat
mesh.transform(matrix)
def write_widget(obj: Object, name='thing', use_size=True):
""" Write a mesh object as a python script for widget use.
"""
script = ""
script += "@widget_generator\n"
script += "def create_" + name + "_widget(geom"
if use_size:
script += ", *, size=1.0"
script += "):\n"
# Vertices
szs = "*size" if use_size else ""
width = 2 if use_size else 3
mesh = obj.data
assert isinstance(mesh, Mesh)
script += " geom.verts = ["
for i, v in enumerate(mesh.vertices):
script += "({:g}{}, {:g}{}, {:g}{}),".format(v.co[0], szs, v.co[1], szs, v.co[2], szs)
script += "\n " if i % width == (width - 1) else " "
script += "]\n"
# Edges
script += " geom.edges = ["
for i, e in enumerate(mesh.edges):
script += "(" + str(e.vertices[0]) + ", " + str(e.vertices[1]) + "),"
script += "\n " if i % 10 == 9 else " "
script += "]\n"
# Faces
if mesh.polygons:
script += " geom.faces = ["
for i, f in enumerate(mesh.polygons):
script += "(" + ", ".join(str(v) for v in f.vertices) + "),"
script += "\n " if i % 10 == 9 else " "
script += "]\n"
return script