2026-03-11_4

This commit is contained in:
2026-03-17 15:34:28 -06:00
parent 9706bc055f
commit eef5547a2c
474 changed files with 113268 additions and 27500 deletions
@@ -6,8 +6,11 @@
import bpy
import os
from bpy.types import Operator
from bpy.props import StringProperty, BoolProperty
from bpy.props import StringProperty, IntProperty
from ..utils import collection_containing_armature
ADDON_NAME = __package__.rsplit(".", 1)[0] if "." in __package__ else __package__
@@ -403,40 +406,174 @@ class DLM_OT_migrator_basebody_shapekeys(Operator):
return {"CANCELLED"}
MIGRATOR_STEP_OPS = (
"dlm.migrator_copy_attributes",
"dlm.migrator_migrate_nla",
"dlm.migrator_custom_properties",
"dlm.migrator_bone_constraints",
"dlm.migrator_retarget_relations",
"dlm.migrator_basebody_shapekeys",
)
class DLM_OT_run_character_migration(Operator):
bl_idname = "dlm.run_character_migration"
bl_label = "Run Character Migration"
bl_description = "Run all six migration steps (CopyAttr, MigNLA, MigCustProps, MigBoneConst, RetargRelatives, MigBBodyShapeKeys) in order"
class DLM_OT_migrator_fk_rotations(Operator):
bl_idname = "dlm.migrator_fk_rotations"
bl_label = "MigFKRot"
bl_description = "Copy FK arm and finger rotations from original to replacement (uses constraints)"
bl_icon = "BONE_DATA"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
steps = [
bpy.ops.dlm.migrator_copy_attributes,
bpy.ops.dlm.migrator_migrate_nla,
bpy.ops.dlm.migrator_custom_properties,
bpy.ops.dlm.migrator_bone_constraints,
bpy.ops.dlm.migrator_retarget_relations,
bpy.ops.dlm.migrator_basebody_shapekeys,
]
for i, op in enumerate(steps):
result = op()
if result != {"FINISHED"}:
self.report({"ERROR"}, f"Migration failed at step {i + 1}: {MIGRATOR_STEP_OPS[i]}")
orig, rep = _get_migrator_pair(context)
if not orig or not rep or orig == rep:
self.report({"ERROR"}, "No valid character pair.")
return {"CANCELLED"}
try:
from ..ops.fk_rotations import copy_fk_rotations
ok, msg = copy_fk_rotations(context, orig, rep)
if ok:
self.report({"INFO"}, msg)
return {"FINISHED"}
else:
self.report({"ERROR"}, msg)
return {"CANCELLED"}
self.report({"INFO"}, "Migration complete.")
return {"FINISHED"}
except Exception as e:
self.report({"ERROR"}, str(e))
return {"CANCELLED"}
class DLM_OT_migrator_fk_rotations_bake(Operator):
bl_idname = "dlm.migrator_fk_rotations_bake"
bl_label = "Bake MigFKRot"
bl_description = "Bake FK rotations to keyframes using nla.bake (similar to tweak tools)"
bl_icon = "KEYFRAME"
bl_options = {"REGISTER", "UNDO"}
track_name: StringProperty(name="NLA Track", description="Optional NLA track name for frame range", default="")
post_clean: BoolProperty(name="Post-clean", description="Clean curves after bake", default=False)
def execute(self, context):
orig, rep = _get_migrator_pair(context)
if not orig or not rep or orig == rep:
self.report({"ERROR"}, "No valid character pair.")
return {"CANCELLED"}
try:
from ..ops.fk_rotations import bake_fk_rotations
ok, msg = bake_fk_rotations(context, orig, rep, track_name=self.track_name or None, post_clean=self.post_clean)
if ok:
self.report({"INFO"}, msg)
return {"FINISHED"}
else:
self.report({"ERROR"}, msg)
return {"CANCELLED"}
except Exception as e:
self.report({"ERROR"}, str(e))
return {"CANCELLED"}
class DLM_OT_migrator_fk_rotations_remove(Operator):
bl_idname = "dlm.migrator_fk_rotations_remove"
bl_label = "Remove MigFKRot"
bl_description = "Remove FK rotation COPY_TRANSFORMS constraints (similar to tweak_remove_arm)"
bl_icon = "X"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
orig, rep = _get_migrator_pair(context)
if not orig or not rep or orig == rep:
self.report({"ERROR"}, "No valid character pair.")
return {"CANCELLED"}
try:
from ..ops.fk_rotations import remove_fk_rotations
ok, msg = remove_fk_rotations(context, rep)
if ok:
self.report({"INFO"}, msg)
return {"FINISHED"}
else:
self.report({"ERROR"}, msg)
return {"CANCELLED"}
except Exception as e:
self.report({"ERROR"}, str(e))
return {"CANCELLED"}
class DLM_OT_migrator_remove_original(Operator):
bl_idname = "dlm.migrator_remove_original"
bl_label = "Remove Original"
bl_description = "Delete the original character armature and its data from the scene"
bl_options = {"REGISTER", "UNDO"}
def execute(self, context):
orig, rep = _get_migrator_pair(context)
if not orig:
self.report({"WARNING"}, "No original character selected")
return {"CANCELLED"}
if orig == rep:
self.report({"ERROR"}, "Original and replacement cannot be the same object")
return {"CANCELLED"}
name = orig.name
# Collect actions from original character before removal
actions_to_remove = set()
if orig.animation_data:
# Active action
if orig.animation_data.action:
actions_to_remove.add(orig.animation_data.action)
# NLA strips
for track in orig.animation_data.nla_tracks:
for strip in track.strips:
if strip.action:
actions_to_remove.add(strip.action)
# Remove collected actions from bpy.data.actions
removed_actions = []
for action in actions_to_remove:
action_name = action.name
try:
bpy.data.actions.remove(action)
removed_actions.append(action_name)
except Exception as e:
self.report({"WARNING"}, f"Could not remove action {action_name}: {e}")
if removed_actions:
self.report({"INFO"}, f"Removed {len(removed_actions)} action(s) from original")
try:
# Try to find and delete the collection containing the original character
coll = collection_containing_armature(orig)
if coll:
coll_name = coll.name # Store name BEFORE removal (RNA invalidates after remove)
context.scene.dynamic_link_manager.original_character = None
try:
bpy.data.collections.remove(coll)
self.report({"INFO"}, f"Removed collection: {coll_name}")
except Exception as remove_err:
# Collection may have already been removed by another process
self.report({"WARNING"}, f"Collection {coll_name} removal issue: {remove_err}")
else:
# Fallback: just delete the armature object
bpy.data.objects.remove(orig, do_unlink=True)
context.scene.dynamic_link_manager.original_character = None
self.report({"INFO"}, f"Removed original character: {name}")
except Exception as e:
self.report({"ERROR"}, f"Failed to remove original: {e}")
return {"CANCELLED"}
# Rename replacement actions with ".rep" suffix
if rep and rep.animation_data:
renamed_actions = []
# Active action
if rep.animation_data.action and ".rep" in rep.animation_data.action.name:
old_name = rep.animation_data.action.name
new_name = old_name.replace(".rep", "")
rep.animation_data.action.name = new_name
renamed_actions.append(f"{old_name} -> {new_name}")
# NLA strips
for track in rep.animation_data.nla_tracks:
for strip in track.strips:
if strip.action and ".rep" in strip.action.name:
old_name = strip.action.name
new_name = old_name.replace(".rep", "")
strip.action.name = new_name
renamed_actions.append(f"{old_name} -> {new_name}")
if renamed_actions:
self.report({"INFO"}, f"Renamed {len(renamed_actions)} replacement action(s)")
return {"FINISHED"}
class DLM_OT_picker_original_character(Operator):
bl_idname = "dlm.picker_original_character"
bl_label = "Pick Original"
@@ -673,7 +810,7 @@ OPERATOR_CLASSES = [
DLM_OT_make_paths_relative,
DLM_OT_make_paths_absolute,
DLM_OT_relocate_single_library,
DLM_OT_run_character_migration,
DLM_OT_migrator_remove_original,
DLM_OT_picker_original_character,
DLM_OT_picker_replacement_character,
DLM_OT_migrator_copy_attributes,
@@ -691,4 +828,7 @@ OPERATOR_CLASSES = [
DLM_OT_tweak_add_both,
DLM_OT_tweak_remove_both,
DLM_OT_tweak_bake_both,
DLM_OT_migrator_fk_rotations,
DLM_OT_migrator_fk_rotations_bake,
DLM_OT_migrator_fk_rotations_remove,
]
@@ -84,7 +84,7 @@ class DLM_PT_main_panel(Panel):
row.prop(props, "replacement_character", text="Replacement")
row.operator("dlm.picker_replacement_character", text="", icon="EYEDROPPER")
row = box.row()
row.operator("dlm.run_character_migration", text="Run migration", icon="ARMATURE_DATA")
row.operator("dlm.migrator_remove_original", text="Remove Original", icon="TRASH")
row = box.row(align=True)
row.operator("dlm.migrator_copy_attributes", text="CopyAttr", icon="COPY_ID")
row.operator("dlm.migrator_migrate_nla", text="MigNLA", icon="NLA")
@@ -92,27 +92,40 @@ class DLM_PT_main_panel(Panel):
row = box.row(align=True)
row.operator("dlm.migrator_bone_constraints", text="MigBoneConst", icon="CONSTRAINT_BONE")
row.operator("dlm.migrator_retarget_relations", text="RetargRelatives", icon="ORIENTATION_PARENT")
row.operator("dlm.migrator_basebody_shapekeys", text="MigBBodyShapeKeys", 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")
# Situational
situational_box = layout.box()
situational_box.label(text="Situational Fixes", icon="QUESTION")
row = situational_box.row(align=True)
row.operator("dlm.migrator_basebody_shapekeys", text="MigBBodyShapeKeys", icon="SHAPEKEY_DATA")
row = situational_box.row(align=True)
row.operator("dlm.migrator_fk_rotations", text="MigFKRot", icon="BONE_DATA")
row.operator("dlm.migrator_fk_rotations_remove", text="Remove", icon="X")
row.operator("dlm.migrator_fk_rotations_bake", text="Bake", icon="KEYFRAME")
# Tweak Tools: header row (always), main box only when expanded
section_icon = "DISCLOSURE_TRI_DOWN" if props.tweak_tools_section_expanded else "DISCLOSURE_TRI_RIGHT"
row = layout.row(align=True)
row.prop(props, "tweak_tools_section_expanded", text="", icon=section_icon, icon_only=True)
row.label(text="Tweak Tools", icon="CONSTRAINT")
if props.tweak_tools_section_expanded:
tweak_box = layout.box()
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)
@@ -60,7 +60,12 @@ class DynamicLinkManagerProperties(PropertyGroup):
poll=lambda self, obj: obj and obj.type == "ARMATURE",
)
# Tweak tools (bake frame range and post-clean)
# Tweak tools (collapsible section)
tweak_tools_section_expanded: BoolProperty(
name="Tweak Tools Expanded",
description="Show or hide the Tweak Tools section",
default=False,
)
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",