tweak tools (untested for now)
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,146 @@
|
||||
# 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})."
|
||||
+191
@@ -449,6 +449,188 @@ class DLM_OT_picker_replacement_character(Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
def _tweak_poll(context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
return orig is not None and rep is not None
|
||||
|
||||
|
||||
class DLM_OT_tweak_add_arm(Operator):
|
||||
bl_idname = "dlm.tweak_add_arm"
|
||||
bl_label = "Add Arm Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
from ..ops import tweak_tools
|
||||
tweak_tools.add_tweak_constraints(orig, rep, "arm")
|
||||
self.report({"INFO"}, "Arm tweak constraints added.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_remove_arm(Operator):
|
||||
bl_idname = "dlm.tweak_remove_arm"
|
||||
bl_label = "Remove Arm Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
from ..ops import tweak_tools
|
||||
n = tweak_tools.remove_tweak_constraints(orig, rep, "arm")
|
||||
self.report({"INFO"}, f"Removed {n} arm tweak constraints.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_bake_arm(Operator):
|
||||
bl_idname = "dlm.tweak_bake_arm"
|
||||
bl_label = "Bake Arm Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
props = context.scene.dynamic_link_manager
|
||||
from ..ops import tweak_tools
|
||||
ok, msg = tweak_tools.bake_tweak_constraints(
|
||||
context, orig, rep, "arm",
|
||||
getattr(props, "tweak_nla_track_name", "") or "",
|
||||
getattr(props, "tweak_bake_post_clean", False),
|
||||
)
|
||||
if ok:
|
||||
self.report({"INFO"}, msg)
|
||||
return {"FINISHED"}
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_add_leg(Operator):
|
||||
bl_idname = "dlm.tweak_add_leg"
|
||||
bl_label = "Add Leg Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
from ..ops import tweak_tools
|
||||
tweak_tools.add_tweak_constraints(orig, rep, "leg")
|
||||
self.report({"INFO"}, "Leg tweak constraints added.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_remove_leg(Operator):
|
||||
bl_idname = "dlm.tweak_remove_leg"
|
||||
bl_label = "Remove Leg Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
from ..ops import tweak_tools
|
||||
n = tweak_tools.remove_tweak_constraints(orig, rep, "leg")
|
||||
self.report({"INFO"}, f"Removed {n} leg tweak constraints.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_bake_leg(Operator):
|
||||
bl_idname = "dlm.tweak_bake_leg"
|
||||
bl_label = "Bake Leg Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
props = context.scene.dynamic_link_manager
|
||||
from ..ops import tweak_tools
|
||||
ok, msg = tweak_tools.bake_tweak_constraints(
|
||||
context, orig, rep, "leg",
|
||||
getattr(props, "tweak_nla_track_name", "") or "",
|
||||
getattr(props, "tweak_bake_post_clean", False),
|
||||
)
|
||||
if ok:
|
||||
self.report({"INFO"}, msg)
|
||||
return {"FINISHED"}
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_add_both(Operator):
|
||||
bl_idname = "dlm.tweak_add_both"
|
||||
bl_label = "Add Arm & Leg Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
from ..ops import tweak_tools
|
||||
tweak_tools.add_tweak_constraints(orig, rep, "both")
|
||||
self.report({"INFO"}, "Arm & leg tweak constraints added.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_remove_both(Operator):
|
||||
bl_idname = "dlm.tweak_remove_both"
|
||||
bl_label = "Remove Arm & Leg Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
from ..ops import tweak_tools
|
||||
n = tweak_tools.remove_tweak_constraints(orig, rep, "both")
|
||||
self.report({"INFO"}, f"Removed {n} tweak constraints.")
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class DLM_OT_tweak_bake_both(Operator):
|
||||
bl_idname = "dlm.tweak_bake_both"
|
||||
bl_label = "Bake Arm & Leg Tweaks"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return _tweak_poll(context)
|
||||
|
||||
def execute(self, context):
|
||||
orig, rep = _get_migrator_pair(context)
|
||||
props = context.scene.dynamic_link_manager
|
||||
from ..ops import tweak_tools
|
||||
ok, msg = tweak_tools.bake_tweak_constraints(
|
||||
context, orig, rep, "both",
|
||||
getattr(props, "tweak_nla_track_name", "") or "",
|
||||
getattr(props, "tweak_bake_post_clean", False),
|
||||
)
|
||||
if ok:
|
||||
self.report({"INFO"}, msg)
|
||||
return {"FINISHED"}
|
||||
self.report({"ERROR"}, msg)
|
||||
return {"CANCELLED"}
|
||||
|
||||
|
||||
OPERATOR_CLASSES = [
|
||||
DLM_OT_replace_linked_asset,
|
||||
DLM_OT_scan_linked_assets,
|
||||
@@ -471,4 +653,13 @@ OPERATOR_CLASSES = [
|
||||
DLM_OT_migrator_bone_constraints,
|
||||
DLM_OT_migrator_retarget_relations,
|
||||
DLM_OT_migrator_basebody_shapekeys,
|
||||
DLM_OT_tweak_add_arm,
|
||||
DLM_OT_tweak_remove_arm,
|
||||
DLM_OT_tweak_bake_arm,
|
||||
DLM_OT_tweak_add_leg,
|
||||
DLM_OT_tweak_remove_leg,
|
||||
DLM_OT_tweak_bake_leg,
|
||||
DLM_OT_tweak_add_both,
|
||||
DLM_OT_tweak_remove_both,
|
||||
DLM_OT_tweak_bake_both,
|
||||
]
|
||||
|
||||
@@ -94,6 +94,26 @@ class DLM_PT_main_panel(Panel):
|
||||
row.operator("dlm.migrator_retarget_relations", text="Retarget relations", icon="ORIENTATION_PARENT")
|
||||
row.operator("dlm.migrator_basebody_shapekeys", text="BaseBody ShapeKeys", icon="SHAPEKEY_DATA")
|
||||
|
||||
# Tweak Tools
|
||||
tweak_box = layout.box()
|
||||
tweak_box.label(text="Tweak Tools", icon="CONSTRAINT")
|
||||
row = tweak_box.row(align=True)
|
||||
row.operator("dlm.tweak_add_arm", text="Add Arm", icon="CONSTRAINT_BONE")
|
||||
row.operator("dlm.tweak_remove_arm", text="Remove Arm", icon="X")
|
||||
row.operator("dlm.tweak_bake_arm", text="Bake Arm", icon="KEYFRAME")
|
||||
row = tweak_box.row(align=True)
|
||||
row.operator("dlm.tweak_add_leg", text="Add Leg", icon="CONSTRAINT_BONE")
|
||||
row.operator("dlm.tweak_remove_leg", text="Remove Leg", icon="X")
|
||||
row.operator("dlm.tweak_bake_leg", text="Bake Leg", icon="KEYFRAME")
|
||||
row = tweak_box.row(align=True)
|
||||
row.operator("dlm.tweak_add_both", text="Add Both", icon="CONSTRAINT_BONE")
|
||||
row.operator("dlm.tweak_remove_both", text="Remove Both", icon="X")
|
||||
row.operator("dlm.tweak_bake_both", text="Bake Both", icon="KEYFRAME")
|
||||
row = tweak_box.row()
|
||||
row.prop(props, "tweak_nla_track_name", text="NLA track")
|
||||
row = tweak_box.row()
|
||||
row.prop(props, "tweak_bake_post_clean", text="Post-clean after bake")
|
||||
|
||||
# Linked Libraries: header row (always), main box only when expanded
|
||||
missing_count = sum(1 for lib in props.linked_libraries if lib.is_missing)
|
||||
section_icon = "DISCLOSURE_TRI_DOWN" if props.linked_libraries_section_expanded else "DISCLOSURE_TRI_RIGHT"
|
||||
|
||||
@@ -59,3 +59,15 @@ class DynamicLinkManagerProperties(PropertyGroup):
|
||||
type=bpy.types.Object,
|
||||
poll=lambda self, obj: obj and obj.type == "ARMATURE",
|
||||
)
|
||||
|
||||
# Tweak tools (bake frame range and post-clean)
|
||||
tweak_nla_track_name: StringProperty(
|
||||
name="NLA Track (bake range)",
|
||||
description="If set, bake uses this NLA track on the replacement armature for frame range; else scene range",
|
||||
default="",
|
||||
)
|
||||
tweak_bake_post_clean: BoolProperty(
|
||||
name="Post-clean after bake",
|
||||
description="Run action clean keyframes and graph decimate (error 0.001) after baking",
|
||||
default=False,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user