# 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