2026-03-11_4
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user