migfkrot working for 1 frame
This commit is contained in:
+4381
-2752
File diff suppressed because one or more lines are too long
+19
-4
@@ -1,14 +1,29 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [0.1.2] - 2026-02-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- MigFKRot operator: copy FK arm and finger rotations from original to replacement using pose matrix copy.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- MigNLA: duplicate actions when copying to repchar so editing on repchar doesn't affect origchar.
|
||||||
|
- MigFKRot: expanded bone name pattern matching for various rig styles (Rigify and alternatives).
|
||||||
|
- MigBBody shapekeys: also duplicate shape key actions for independence.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- MigFKRot: add debug logging to show which bones are found and copied.
|
||||||
|
|
||||||
|
|
||||||
## [0.1.1] - 2026-02-19
|
## [0.1.1] - 2026-02-19
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- MigBBody shapekeys: action slot not applying; now copy action and slot props (last_slot_identifier, action_slot, blend/extrapolation/influence) from original base body.
|
- MigBBody shapekeys: action slot not applying; now copy action and slot props (last_slot_identifier, action_slot, blend/extrapolation/influence) from original base body.
|
||||||
- RetargRelations: skip objects in Original Character’s hierarchy (linked collection); only retarget relations outside orig’s hierarchy.
|
- RetargRelations: skip objects in Original Character's hierarchy (linked collection); only retarget relations outside orig's hierarchy.
|
||||||
- MigBoneConst: copy all constraint properties and targets (RNA POINTER/COLLECTION with orig→rep retarget), not just type/name/mute/influence.
|
- MigBoneConst: copy all constraint properties and targets (RNA POINTER/COLLECTION with orig→rep retarget), not just type/name/mute/influence.
|
||||||
- AnimLayers: detect/mirror via RNA (obj.als.turn_on) so “Animation Layer attributes migrated” reports correctly.
|
- AnimLayers: detect/mirror via RNA (obj.als.turn_on) so "Animation Layer attributes migrated" reports correctly.
|
||||||
- MigCustProps: recursive id-property copy for nested groups; debug logging for armature/bone keys.
|
- MigCustProps: recursive id-property copy for nested groups; debug logging for armature/bone keys.
|
||||||
|
|
||||||
|
|
||||||
## [0.1.0] - 2026-02-19
|
## [0.1.0] - 2026-02-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@@ -17,12 +32,12 @@
|
|||||||
- MigNLA: when no NLA, copy active action and action slot (incl. last_slot_identifier, blend/extrapolation/influence); debug logging for slot migration.
|
- MigNLA: when no NLA, copy active action and action slot (incl. last_slot_identifier, blend/extrapolation/influence); debug logging for slot migration.
|
||||||
- NLAMig: AnimLayers support (mirror `als.turn_on`), strip timing and properties; active-action-only path when no NLA tracks.
|
- NLAMig: AnimLayers support (mirror `als.turn_on`), strip timing and properties; active-action-only path when no NLA tracks.
|
||||||
- Operator icons (CopyAttr, MigNLA, CustProps, BoneConst, RetargRelatives, MigBBodyShapeKeys, pickers, tweak ops).
|
- Operator icons (CopyAttr, MigNLA, CustProps, BoneConst, RetargRelatives, MigBBodyShapeKeys, pickers, tweak ops).
|
||||||
- BaseBody shapekeys step: prefer original base body’s shape-key action slot; library override + editable; copy shape key values.
|
- BaseBody shapekeys step: prefer original base body's shape-key action slot; library override + editable; copy shape key values.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Operator labels refactored to canonical short names (CopyAttr, MigNLA, MigCustProps, etc.) with `bl_description` on all UI operators.
|
- Operator labels refactored to canonical short names (CopyAttr, MigNLA, MigCustProps, etc.) with `bl_description` on all UI operators.
|
||||||
- Migrate BaseBody shapekeys redefined: find base body in hierarchy, override mesh/key data when linked, then apply shape-key action.
|
- Migrate BaseBody shapekeys redefined: find base body in hierarchy, override mesh/key data when linked, then apply shape-key action.
|
||||||
- Button labels truncated (e.g. “NLA”, “BaseBody ShapeKeys”).
|
- Button labels truncated (e.g. "NLA", "BaseBody ShapeKeys").
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- BaseBody shapekeys: lib override creation and value copy; correct original-base-body lookup and slot assignment.
|
- BaseBody shapekeys: lib override creation and value copy; correct original-base-body lookup and slot assignment.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
|
|||||||
id = "dynamiclinkmanager"
|
id = "dynamiclinkmanager"
|
||||||
name = "Dynamic Link Manager"
|
name = "Dynamic Link Manager"
|
||||||
tagline = "Character migrator and linked library tools"
|
tagline = "Character migrator and linked library tools"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
type = "add-on"
|
type = "add-on"
|
||||||
|
|
||||||
# Optional: Semantic Versioning
|
# Optional: Semantic Versioning
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""FK rotations: copy visible rotations from original to replacement using constraints."""
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
# Arm FK bone name patterns to check (Rigify and common alternatives)
|
||||||
|
ARM_FK_PATTERNS = (
|
||||||
|
# Rigify style
|
||||||
|
("upper_arm_fk.L", "upper_arm_fk.R"),
|
||||||
|
("forearm_fk.L", "forearm_fk.R"),
|
||||||
|
("hand_fk.L", "hand_fk.R"),
|
||||||
|
# Common alternatives
|
||||||
|
("upper_arm.L", "upper_arm.R"),
|
||||||
|
("forearm.L", "forearm.R"),
|
||||||
|
("hand.L", "hand.R"),
|
||||||
|
("arm_fk.L", "arm_fk.R"),
|
||||||
|
("lower_arm_fk.L", "lower_arm_fk.R"),
|
||||||
|
# Short forms
|
||||||
|
("arm.L", "arm.R"),
|
||||||
|
("elbow.L", "elbow.R"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Finger bone name patterns (will match with .01, .02, .03, etc.)
|
||||||
|
FINGER_PREFIXES = (
|
||||||
|
"thumb", "f_index", "f_middle", "f_ring", "f_pinky",
|
||||||
|
"finger1", "finger2", "finger3", "finger4", "finger5",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_matching_arm_fk_bones(armature):
|
||||||
|
"""Return list of arm FK bone names that exist on armature."""
|
||||||
|
if not armature or armature.type != "ARMATURE" or not armature.pose:
|
||||||
|
return []
|
||||||
|
bones = armature.pose.bones
|
||||||
|
found = []
|
||||||
|
for pattern in ARM_FK_PATTERNS:
|
||||||
|
for name in pattern:
|
||||||
|
if name in bones:
|
||||||
|
found.append(name)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def _get_finger_fk_bones(armature):
|
||||||
|
"""Return list of FK finger bone names that exist on armature. Only control bones (no ORG-/DEF-/MCH- prefix)."""
|
||||||
|
if not armature or armature.type != "ARMATURE" or not armature.pose:
|
||||||
|
return []
|
||||||
|
bones = armature.pose.bones
|
||||||
|
finger_bones = []
|
||||||
|
for bone_name in bones.keys():
|
||||||
|
if bone_name.startswith("ORG-") or bone_name.startswith("DEF-") or bone_name.startswith("MCH-"):
|
||||||
|
continue
|
||||||
|
lower_name = bone_name.lower()
|
||||||
|
for prefix in FINGER_PREFIXES:
|
||||||
|
if prefix in lower_name and ("_fk." in bone_name or bone_name.endswith(".L") or bone_name.endswith(".R")):
|
||||||
|
if "." in bone_name and any(d in bone_name for d in "0123456789"):
|
||||||
|
finger_bones.append(bone_name)
|
||||||
|
break
|
||||||
|
return finger_bones
|
||||||
|
|
||||||
|
|
||||||
|
def _get_fk_bones(armature):
|
||||||
|
"""Return list of all FK arm and finger bone names that exist on armature."""
|
||||||
|
arm_bones = _get_matching_arm_fk_bones(armature)
|
||||||
|
finger_bones = _get_finger_fk_bones(armature)
|
||||||
|
return list(dict.fromkeys(arm_bones + finger_bones))
|
||||||
|
|
||||||
|
|
||||||
|
def copy_fk_rotations(context, orig, rep):
|
||||||
|
"""
|
||||||
|
Copy visual rotations from orig to rep using temporary COPY_TRANSFORMS constraints.
|
||||||
|
This properly handles all coordinate space conversions.
|
||||||
|
Returns (True, message) or (False, error_message).
|
||||||
|
"""
|
||||||
|
fk_names = _get_fk_bones(rep)
|
||||||
|
|
||||||
|
print(f"[DLM MigFKRot] Found {len(fk_names)} FK bones on {rep.name}")
|
||||||
|
|
||||||
|
if not fk_names:
|
||||||
|
return False, "No FK arm or finger bones found on replacement armature"
|
||||||
|
|
||||||
|
# Filter to bones that exist on both
|
||||||
|
common_bones = [n for n in fk_names if n in orig.pose.bones and n in rep.pose.bones]
|
||||||
|
if not common_bones:
|
||||||
|
return False, "No matching FK bones found on both armatures"
|
||||||
|
|
||||||
|
print(f"[DLM MigFKRot] Will copy {len(common_bones)} bones using constraints")
|
||||||
|
|
||||||
|
original_active = context.view_layer.objects.active
|
||||||
|
constraints_added = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure rep is active and in pose mode
|
||||||
|
bpy.context.view_layer.objects.active = rep
|
||||||
|
if rep.mode != 'POSE':
|
||||||
|
bpy.ops.object.mode_set(mode='POSE')
|
||||||
|
|
||||||
|
# Step 1: Add COPY_TRANSFORMS constraints to each rep bone
|
||||||
|
for bone_name in common_bones:
|
||||||
|
try:
|
||||||
|
rep_bone = rep.pose.bones[bone_name]
|
||||||
|
|
||||||
|
# Check if bone already has this constraint
|
||||||
|
existing = [c for c in rep_bone.constraints if c.type == 'COPY_TRANSFORMS' and getattr(c, 'target', None) == orig]
|
||||||
|
if existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Add constraint
|
||||||
|
c = rep_bone.constraints.new(type='COPY_TRANSFORMS')
|
||||||
|
c.name = "MigFKRot_Temp"
|
||||||
|
c.target = orig
|
||||||
|
c.subtarget = bone_name
|
||||||
|
c.target_space = 'POSE'
|
||||||
|
c.owner_space = 'POSE'
|
||||||
|
|
||||||
|
constraints_added.append((rep_bone, c))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DLM MigFKRot] Failed to add constraint for {bone_name}: {e}")
|
||||||
|
|
||||||
|
# Step 2: Update scene to evaluate constraints
|
||||||
|
context.view_layer.update()
|
||||||
|
|
||||||
|
# Step 3: Apply visual transform (bake constraint result into pose)
|
||||||
|
bpy.ops.pose.select_all(action='DESELECT')
|
||||||
|
for rep_bone, _ in constraints_added:
|
||||||
|
rep_bone.bone.select = True
|
||||||
|
|
||||||
|
# Apply visual transform - this bakes the constraint result
|
||||||
|
bpy.ops.pose.visual_transform_apply()
|
||||||
|
|
||||||
|
# Step 4: Remove constraints
|
||||||
|
for rep_bone, c in constraints_added:
|
||||||
|
try:
|
||||||
|
rep_bone.constraints.remove(c)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
print(f"[DLM MigFKRot] Copied {len(constraints_added)} bones")
|
||||||
|
return True, f"Copied FK rotations for {len(constraints_added)} bones"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[DLM MigFKRot] Error: {e}")
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Cleanup any remaining constraints
|
||||||
|
for rep_bone, c in constraints_added:
|
||||||
|
try:
|
||||||
|
if c in rep_bone.constraints:
|
||||||
|
rep_bone.constraints.remove(c)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if original_active:
|
||||||
|
context.view_layer.objects.active = original_active
|
||||||
+29
-5
@@ -184,8 +184,27 @@ def _mirror_als_turn_on(orig, rep):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _duplicate_action(src_action, suffix=".rep"):
|
||||||
|
"""Duplicate an action, returning the new action with a unique name."""
|
||||||
|
if src_action is None:
|
||||||
|
return None
|
||||||
|
new_name = src_action.name
|
||||||
|
if not new_name.endswith(suffix):
|
||||||
|
new_name = f"{new_name}{suffix}"
|
||||||
|
# Ensure unique name
|
||||||
|
base_name = new_name
|
||||||
|
n = 1
|
||||||
|
while new_name in bpy.data.actions:
|
||||||
|
new_name = f"{base_name}.{n:03d}"
|
||||||
|
n += 1
|
||||||
|
new_action = src_action.copy()
|
||||||
|
new_action.name = new_name
|
||||||
|
return new_action
|
||||||
|
|
||||||
|
|
||||||
def run_mig_nla(orig, rep, report=None):
|
def run_mig_nla(orig, rep, report=None):
|
||||||
"""Migrate NLA: copy tracks and strips to replacement; or mirror action slot when no NLA (MigNLA)."""
|
"""Migrate NLA: copy tracks and strips to replacement; or mirror action slot when no NLA (MigNLA).
|
||||||
|
Actions are duplicated so repchar has independent copies."""
|
||||||
if not orig.animation_data:
|
if not orig.animation_data:
|
||||||
return
|
return
|
||||||
ad = orig.animation_data
|
ad = orig.animation_data
|
||||||
@@ -211,11 +230,13 @@ def run_mig_nla(orig, rep, report=None):
|
|||||||
print(f"[DLM MigNLA] {p}={v!r}")
|
print(f"[DLM MigNLA] {p}={v!r}")
|
||||||
_slot_debug("Orig (before)", ad)
|
_slot_debug("Orig (before)", ad)
|
||||||
_slot_debug("Rep (before)", rad)
|
_slot_debug("Rep (before)", rad)
|
||||||
|
# Duplicate the active action for repchar
|
||||||
|
dup_action = _duplicate_action(active_action, suffix=".rep")
|
||||||
# Copy last_slot_identifier before action so slot is resolved when assigning (4.4+).
|
# Copy last_slot_identifier before action so slot is resolved when assigning (4.4+).
|
||||||
if hasattr(ad, "last_slot_identifier") and hasattr(rad, "last_slot_identifier") and ad.last_slot_identifier:
|
if hasattr(ad, "last_slot_identifier") and hasattr(rad, "last_slot_identifier") and ad.last_slot_identifier:
|
||||||
rad.last_slot_identifier = ad.last_slot_identifier
|
rad.last_slot_identifier = ad.last_slot_identifier
|
||||||
print(f"[DLM MigNLA] set rep last_slot_identifier={ad.last_slot_identifier!r}")
|
print(f"[DLM MigNLA] set rep last_slot_identifier={ad.last_slot_identifier!r}")
|
||||||
rad.action = active_action
|
rad.action = dup_action
|
||||||
# Copy Action Slot and related props (Blender 4.4+ slotted actions).
|
# Copy Action Slot and related props (Blender 4.4+ slotted actions).
|
||||||
if getattr(ad, "action_slot", None) and getattr(rad, "action_slot", None):
|
if getattr(ad, "action_slot", None) and getattr(rad, "action_slot", None):
|
||||||
try:
|
try:
|
||||||
@@ -230,7 +251,7 @@ def run_mig_nla(orig, rep, report=None):
|
|||||||
_slot_debug("Rep (after)", rad)
|
_slot_debug("Rep (after)", rad)
|
||||||
_mirror_als_turn_on(orig, rep)
|
_mirror_als_turn_on(orig, rep)
|
||||||
if report:
|
if report:
|
||||||
report({"INFO"}, "No NLA detected, active action and slot copied to Replacement Armature.")
|
report({"INFO"}, "No NLA detected, active action (duplicated) and slot copied to Replacement Armature.")
|
||||||
return
|
return
|
||||||
if rep.animation_data is None:
|
if rep.animation_data is None:
|
||||||
rep.animation_data_create()
|
rep.animation_data_create()
|
||||||
@@ -253,8 +274,9 @@ def run_mig_nla(orig, rep, report=None):
|
|||||||
for strip in track.strips:
|
for strip in track.strips:
|
||||||
if strip.type != "CLIP" or not strip.action:
|
if strip.type != "CLIP" or not strip.action:
|
||||||
continue
|
continue
|
||||||
|
dup_action = _duplicate_action(strip.action, suffix=".rep")
|
||||||
new_strip = new_track.strips.new(
|
new_strip = new_track.strips.new(
|
||||||
strip.name, int(strip.frame_start), strip.action
|
strip.name, int(strip.frame_start), dup_action
|
||||||
)
|
)
|
||||||
new_strip.blend_type = strip.blend_type
|
new_strip.blend_type = strip.blend_type
|
||||||
new_strip.extrapolation = strip.extrapolation
|
new_strip.extrapolation = strip.extrapolation
|
||||||
@@ -612,10 +634,12 @@ def run_mig_bbody_shapekeys(orig, rep, rep_descendants, context=None):
|
|||||||
or bpy.data.actions.get(body_name + "Action.001")
|
or bpy.data.actions.get(body_name + "Action.001")
|
||||||
)
|
)
|
||||||
if action:
|
if action:
|
||||||
|
# Duplicate action so repchar has independent copy
|
||||||
|
dup_action = _duplicate_action(action, suffix=".rep")
|
||||||
# Copy slot-related props before action so slot is applied (Blender 4.4+).
|
# Copy slot-related props before action so slot is applied (Blender 4.4+).
|
||||||
if orig_sk_ad and hasattr(sk_ad, "last_slot_identifier") and hasattr(orig_sk_ad, "last_slot_identifier") and orig_sk_ad.last_slot_identifier:
|
if orig_sk_ad and hasattr(sk_ad, "last_slot_identifier") and hasattr(orig_sk_ad, "last_slot_identifier") and orig_sk_ad.last_slot_identifier:
|
||||||
sk_ad.last_slot_identifier = orig_sk_ad.last_slot_identifier
|
sk_ad.last_slot_identifier = orig_sk_ad.last_slot_identifier
|
||||||
sk_ad.action = action
|
sk_ad.action = dup_action
|
||||||
if orig_sk_ad and getattr(orig_sk_ad, "action_slot", None) and getattr(sk_ad, "action_slot", None):
|
if orig_sk_ad and getattr(orig_sk_ad, "action_slot", None) and getattr(sk_ad, "action_slot", None):
|
||||||
try:
|
try:
|
||||||
sk_ad.action_slot = orig_sk_ad.action_slot
|
sk_ad.action_slot = orig_sk_ad.action_slot
|
||||||
|
|||||||
@@ -403,6 +403,32 @@ class DLM_OT_migrator_basebody_shapekeys(Operator):
|
|||||||
return {"CANCELLED"}
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
|
||||||
|
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 copy_pose_vis_rot)"
|
||||||
|
bl_icon = "BONE_DATA"
|
||||||
|
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 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"}
|
||||||
|
except Exception as e:
|
||||||
|
self.report({"ERROR"}, str(e))
|
||||||
|
return {"CANCELLED"}
|
||||||
|
|
||||||
|
|
||||||
MIGRATOR_STEP_OPS = (
|
MIGRATOR_STEP_OPS = (
|
||||||
"dlm.migrator_copy_attributes",
|
"dlm.migrator_copy_attributes",
|
||||||
"dlm.migrator_migrate_nla",
|
"dlm.migrator_migrate_nla",
|
||||||
@@ -691,4 +717,5 @@ OPERATOR_CLASSES = [
|
|||||||
DLM_OT_tweak_add_both,
|
DLM_OT_tweak_add_both,
|
||||||
DLM_OT_tweak_remove_both,
|
DLM_OT_tweak_remove_both,
|
||||||
DLM_OT_tweak_bake_both,
|
DLM_OT_tweak_bake_both,
|
||||||
|
DLM_OT_migrator_fk_rotations,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ class DLM_PT_main_panel(Panel):
|
|||||||
row.operator("dlm.migrator_bone_constraints", text="MigBoneConst", icon="CONSTRAINT_BONE")
|
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_retarget_relations", text="RetargRelatives", icon="ORIENTATION_PARENT")
|
||||||
row.operator("dlm.migrator_basebody_shapekeys", text="MigBBodyShapeKeys", icon="SHAPEKEY_DATA")
|
row.operator("dlm.migrator_basebody_shapekeys", text="MigBBodyShapeKeys", icon="SHAPEKEY_DATA")
|
||||||
|
row = box.row()
|
||||||
|
row.operator("dlm.migrator_fk_rotations", text="MigFKRot", icon="BONE_DATA")
|
||||||
|
|
||||||
# Tweak Tools
|
# Tweak Tools
|
||||||
tweak_box = layout.box()
|
tweak_box = layout.box()
|
||||||
|
|||||||
Reference in New Issue
Block a user