Files
2026-02-19 11:09:48 -07:00

147 lines
4.9 KiB
Python

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
"""Tweak tools: add/remove/bake COPY_TRANSFORMS on Rigify arm/leg tweak bones."""
import bpy
# Rigify-style tweak bone names (only those present on armature are used)
ARM_TWEAK_BONES = (
"upper_arm_tweak.L", "upper_arm_tweak.R",
"forearm_tweak.L", "forearm_tweak.R",
"hand_tweak.L", "hand_tweak.R",
)
LEG_TWEAK_BONES = (
"thigh_tweak.L", "thigh_tweak.R",
"shin_tweak.L", "shin_tweak.R",
"foot_tweak.L", "foot_tweak.R",
)
TWEAK_CONSTRAINT_NAME = "Copy from Original"
def get_tweak_bones(armature, limb):
"""Return list of tweak bone names that exist on armature. limb in 'arm', 'leg', 'both'."""
if not armature or armature.type != "ARMATURE" or not armature.pose:
return []
bones = armature.pose.bones
if limb == "arm":
names = ARM_TWEAK_BONES
elif limb == "leg":
names = LEG_TWEAK_BONES
elif limb == "both":
names = ARM_TWEAK_BONES + LEG_TWEAK_BONES
else:
return []
return [n for n in names if n in bones]
def add_tweak_constraints(orig, rep, limb):
"""On rep, add COPY_TRANSFORMS on each tweak bone targeting same bone on orig."""
names = get_tweak_bones(rep, limb)
for name in names:
pb = rep.pose.bones[name]
c = pb.constraints.new(type="COPY_TRANSFORMS")
c.name = TWEAK_CONSTRAINT_NAME
c.target = orig
c.subtarget = name
c.mix_mode = "REPLACE"
def remove_tweak_constraints(orig, rep, limb):
"""On rep, remove COPY_TRANSFORMS that target orig from tweak bones."""
names = get_tweak_bones(rep, limb)
removed = 0
for name in names:
pb = rep.pose.bones[name]
to_remove = [
c for c in pb.constraints
if c.type == "COPY_TRANSFORMS" and getattr(c, "target", None) == orig
]
for c in to_remove:
pb.constraints.remove(c)
removed += 1
return removed
def _frame_range_from_track(rep, track_name):
"""Return (frame_start, frame_end) from rep's NLA track named track_name, or None."""
if not track_name or not rep.animation_data or not rep.animation_data.nla_tracks:
return None
track = rep.animation_data.nla_tracks.get(track_name)
if not track or not track.strips:
return None
start = min(s.frame_start for s in track.strips)
end = max(s.frame_end for s in track.strips)
return (int(start), int(end))
def bake_tweak_constraints(context, orig, rep, limb, track_name, post_clean):
"""
Select rep's tweak bones, run nla.bake, optionally run clean + decimate.
Returns (True, message) or (False, error_message).
"""
names = get_tweak_bones(rep, limb)
if not names:
return False, f"No tweak bones found for {limb} on {rep.name}"
scene = context.scene
frame_range = _frame_range_from_track(rep, track_name) if track_name else None
if not frame_range:
frame_range = (scene.frame_start, scene.frame_end)
frame_start, frame_end = frame_range
# Ensure rep is active and in pose mode
if context.view_layer.objects.active != rep:
context.view_layer.objects.active = rep
if rep.mode != "POSE":
bpy.ops.object.mode_set(mode="POSE")
# Select only tweak bones on rep
bpy.ops.pose.select_all(action="DESELECT")
for name in names:
rep.pose.bones[name].bone.select = True
# Bake
try:
bpy.ops.nla.bake(
frame_start=frame_start,
frame_end=frame_end,
step=1,
only_selected=True,
visual_keying=True,
clear_constraints=True,
clear_parents=True,
use_current_action=True,
clean_curves=False,
bake_types={"POSE"},
channel_types={"LOCATION", "ROTATION"},
)
except Exception as e:
return False, str(e)
if not post_clean:
return True, f"Baked {len(names)} tweak bones ({frame_start}-{frame_end})."
# Post-clean: find an area we can use for action/graph ops
win = context.window
for area in win.screen.areas:
if area.type == "DOPESHEET_EDITOR":
with context.temp_override(window=win, area=area):
try:
bpy.ops.action.clean_keyframes()
except Exception:
pass
break
for area in win.screen.areas:
if area.type == "GRAPH_EDITOR":
with context.temp_override(window=win, area=area):
try:
bpy.ops.graph.decimate(mode="ERROR", error=0.001)
except Exception:
pass
break
return True, f"Baked and cleaned {len(names)} tweak bones ({frame_start}-{frame_end})."