diff --git a/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md b/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md
index 764862f..7e274f8 100644
--- a/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md
+++ b/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md
@@ -14395,7 +14395,7 @@ Let me update the fk_rotations.py to implement this baking approach.
---
-Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py
@@ -14447,7 +14447,9 @@ Parameters:
```json
{
"paths": [
- "T:\\Dynamic-Link-Manager\\ops\\fk_rotations.py"
+ "T:\\Dynamic-Link-Manager\\ops\\fk_rotations.py",
+ "T:\\Dynamic-Link-Manager\\ui\\operators.py",
+ "T:\\Dynamic-Link-Manager\\ui\\panels.py"
]
}
```
@@ -14458,6 +14460,12 @@ Result:
"linterErrorsByFile": [
{
"relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\fk_rotations.py"
+ },
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\operators.py"
+ },
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\panels.py"
}
]
}
@@ -14617,7 +14625,13 @@ Let me fix this:
---
-The string to replace was not found in the file.
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
---
@@ -17241,3 +17255,418 @@ The constraints are driving the pose but not "baking" it. When removed, the bone
---
+Thought Process
+I added `bpy.ops.pose.visual_transform_apply()` which is Blender's built-in operator for baking constraint results into the actual pose. This should apply the visual transform so when we remove the constraints, the pose stays.
+
+Let me check if there are any linter errors.
+
+---
+
+
+
+Tool use: **read_lints**
+
+
+Parameters:
+
+```json
+{
+ "paths": [
+ "T:\\Dynamic-Link-Manager\\ops\\fk_rotations.py"
+ ]
+}
+```
+Result:
+
+```json
+{
+ "linterErrorsByFile": [
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\fk_rotations.py"
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+Now using `bpy.ops.pose.visual_transform_apply()` which is Blender's built-in operator for baking constraint results into the actual bone pose. This should properly apply the copied rotations so they persist when constraints are removed.
+
+---
+
+Now using `bpy.ops.pose.visual_transform_apply()` - Blender's built-in operator that bakes constraint results into the actual pose. This should properly save the copied rotations when constraints are removed.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt That worked! now we just need to add a bake step, similar to the tweak tools section, i.e. bpy.ops.dlm.tweak_bake_arm()
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+Great! The visual transform apply worked. Now the user wants to add a bake step, similar to how the tweak tools work. Let me check how the tweak bake is implemented and add a similar operator for FK rotation baking.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\tweak_tools.py
+
+
+
+
+---
+
+Thought Process
+Now I understand the pattern. The user wants to add a bake operator for FK rotations similar to the tweak bake. Let me add:
+
+1. A `bake_fk_rotations` function in `fk_rotations.py`
+2. A new operator `DLM_OT_migrator_fk_rotations_bake` in `ui/operators.py`
+3. Register it and add to the UI
+
+Let me implement this.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+---
+
+Thought Process
+Now I'll add a bake function similar to the tweak tools. Let me append it to the fk_rotations.py file.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+
+---
+
+Thought Process
+Now I need to add the UI operator and register it. Let me add a new operator class in ui/operators.py.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+---
+
+Thought Process
+Now I need to add the bake operator. Let me insert it after the existing MigFKRot operator.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+Thought Process
+Now I need to register the new operator and add it to the UI. Let me find the registration section and add it there.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+---
+
+Thought Process
+Now I need to add the new operator to the OPERATOR_CLASSES list.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+Thought Process
+Now let me add the bake button to the UI panel, next to the existing MigFKRot button.Let me add the bake button next to the MigFKRot button.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py
+
+
+
+
+
+---
+
+Thought Process
+Now let me check the lints to make sure everything is correct.Done. The bake operator is now added. Summary of changes:1. **ops/fk_rotations.py** - Added `bake_fk_rotations()` function that uses `nla.bake` to bake FK rotations to keyframes
+2. **ui/operators.py** - Added `DLM_OT_migrator_fk_rotations_bake` operator class with `track_name` and `post_clean` properties, registered it
+3. **ui/panels.py** - Added "Bake" button next to the "MigFKRot" button in the Character Migrator panelThe bake operator:- Uses `bpy.ops.nla.bake` with `visual_keying=True`
+- Takes an optional NLA track name for frame range (like tweak tools)
+- Has a `post_clean` option for curve cleaning
+- Creates a new action (doesn't overwrite existing)
+
+---
+
+Done. Added the bake operator for FK rotations:
+
+**Changes:**
+1. **ops/fk_rotations.py** - Added `bake_fk_rotations()` function using `nla.bake` with `visual_keying=True`, similar to tweak tools
+2. **ui/operators.py** - Added `DLM_OT_migrator_fk_rotations_bake` operator with optional NLA track name and post-clean options
+3. **ui/panels.py** - Added "Bake" button next to "MigFKRot"
+
+**Usage:**
+1. Click **MigFKRot** to copy FK rotations from original to replacement (current frame)
+2. Click **Bake** to bake those rotations to keyframes using `nla.bake`
+
+The bake operator:
+- Uses visual keying (captures constraint results)
+- Takes optional NLA track name for frame range
+- Has post-clean option for curve cleaning
+- Creates a new action (doesn't overwrite existing)
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt can't enable addon
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user can't enable the addon. Let me check the terminal for errors.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt
+
+
+
+
+---
+
+Thought Process
+There's a `enableFailure` message. Let me check if there's more context about what caused it. Let me read more lines.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt
+
+
+
+
+---
+
+Thought Process
+The terminal shows the addon is trying to load but there's an `enableFailure` at the end. Let me look for the error message that explains why it failed.
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "SyntaxError|ImportError|enableFailure|Traceback",
+ "path": "c:\\Users\\Nathan\\.cursor\\projects\\t-Dynamic-Link-Manager\\terminals\\3.txt",
+ "outputMode": "content",
+ "contextBefore": 2,
+ "contextAfter": 5,
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "SyntaxError|ImportError|enableFailure|Traceback",
+ "path": "c:\\Users\\Nathan\\.cursor\\projects\\t-Dynamic-Link-Manager\\terminals\\3.txt",
+ "outputMode": "content",
+ "totalFiles": 1,
+ "totalMatches": 12,
+ "topFiles": [
+ {
+ "uri": "c:\\Users\\Nathan\\.cursor\\projects\\t-Dynamic-Link-Manager\\terminals\\3.txt",
+ "matchCount": 88
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+I see the error now:
+```
+NameError: name 'BoolProperty' is not defined
+```
+
+The `BoolProperty` and `StringProperty` need to be imported from `bpy.props`. I added properties to the bake operator but forgot to import them at the top of the file.Fixed - just needed to add the missing import for `StringProperty` and `BoolProperty`.
+
+---
+
+Fixed. Just needed to import `BoolProperty` and `StringProperty` from `bpy.props`.
+
+---
+
+
+
+ Tool use: **read_lints** • Read lints for 1 file
+
+Lint paths:
+
+- `T:\Dynamic-Link-Manager\ui\operators.py`
+
+Lint results
+
+Lint errors for `T:\Dynamic-Link-Manager\ui\operators.py`
+
+
+| Message | Range | Severity |
+|-------|-------|----------|
+| Import "bpy.types" could not be resolved | 8:6-8:15 | DIAGNOSTIC_SEVERITY_WARNING |
+| Import "bpy.props" could not be resolved | 9:6-9:15 | DIAGNOSTIC_SEVERITY_WARNING |
+| Import "bpy.props" could not be resolved | 10:6-10:15 | DIAGNOSTIC_SEVERITY_WARNING |
+
+
+
+
+
+---
+
+Thought Process
+The linter warnings are expected since it can't resolve Blender imports. The important thing is that the Python will work correctly now that the imports are present.
+
+---
+
+The warnings are expected (Blender imports). The addon should enable now with the proper imports.
+
+---
+
+_**User**_
+
+in bpy.ops.dlm.migrator_retarget_relations(), we should make it so if there is any parent of the origarmature, it should be replicated on the reparmature.
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user wants to update `migrator_retarget_relations` to also replicate the parent of the original armature on the replacement armature, if the original has one.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+I need to add logic to replicate the parent of `orig` on `rep`. If `orig.parent` exists, set `rep.parent = orig.parent`.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+Done. Now if the original armature has a parent, the replacement will also be parented to the same object/bone.
+
+---
+
+Done. Now `migrator_retarget_relations` will also replicate the parent of the original armature on the replacement:
+
+- `rep.parent = orig.parent`
+- `rep.parent_type = orig.parent_type` (object/bone/vertex)
+- `rep.parent_bone = orig.parent_bone` (if parented to a bone)
+
+---
+
+_**User**_
+
+parent but keep transforms.
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+Status: **loading**
+
+
+
+
+
+---
+
+
+
+---
+
+
+
+---
+
diff --git a/ops/fk_rotations.py b/ops/fk_rotations.py
index 91254da..27b6055 100644
--- a/ops/fk_rotations.py
+++ b/ops/fk_rotations.py
@@ -156,3 +156,84 @@ def copy_fk_rotations(context, orig, rep):
if original_active:
context.view_layer.objects.active = original_active
+
+
+def bake_fk_rotations(context, rep, track_name=None, post_clean=False):
+ """
+ Bake FK arm/finger rotations to keyframes.
+ Similar to tweak_tools.bake_tweak_constraints but for FK bones.
+ Returns (True, message) or (False, error_message).
+ """
+ fk_names = _get_fk_bones(rep)
+ if not fk_names:
+ return False, f"No FK bones found on {rep.name}"
+
+ scene = context.scene
+
+ # Get frame range from track or scene
+ frame_range = None
+ if track_name and rep.animation_data and rep.animation_data.nla_tracks:
+ track = rep.animation_data.nla_tracks.get(track_name)
+ if track and track.strips:
+ start = min(s.frame_start for s in track.strips)
+ end = max(s.frame_end for s in track.strips)
+ frame_range = (int(start), int(end))
+
+ 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 FK bones
+ bpy.ops.pose.select_all(action="DESELECT")
+ for name in fk_names:
+ if name in rep.pose.bones:
+ 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=False,
+ clear_parents=False,
+ use_current_action=False, # Create new action
+ clean_curves=False,
+ bake_types={"POSE"},
+ channel_types={"ROTATION"},
+ )
+ except Exception as e:
+ return False, str(e)
+
+ if not post_clean:
+ return True, f"Baked {len(fk_names)} FK bones ({frame_start}-{frame_end})."
+
+ # Post-clean
+ 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(fk_names)} FK bones ({frame_start}-{frame_end})."
diff --git a/ops/migrator.py b/ops/migrator.py
index e6e4b4a..bc2d9ae 100644
--- a/ops/migrator.py
+++ b/ops/migrator.py
@@ -460,6 +460,16 @@ def run_mig_bone_const(orig, rep, orig_to_rep):
def run_retarg_relatives(orig, rep, rep_descendants, orig_to_rep):
"""Retarget relations: parents, constraint targets, Armature modifiers to rep. Skip objects in orig's hierarchy (linked collection)."""
+ # Replicate orig's parent on rep if it exists (keep world transform)
+ if orig.parent is not None:
+ # Store world matrix before reparenting
+ world_matrix = rep.matrix_world.copy()
+ rep.parent = orig.parent
+ rep.parent_type = orig.parent_type
+ rep.parent_bone = orig.parent_bone
+ # Restore world matrix
+ rep.matrix_world = world_matrix
+
orig_hierarchy = {orig} | descendants(orig)
candidates = set(rep_descendants)
for ob in bpy.data.objects:
diff --git a/ui/operators.py b/ui/operators.py
index 0309b61..5f51631 100644
--- a/ui/operators.py
+++ b/ui/operators.py
@@ -6,6 +6,7 @@
import bpy
import os
from bpy.types import Operator
+from bpy.props import StringProperty, BoolProperty
from bpy.props import StringProperty, IntProperty
ADDON_NAME = __package__.rsplit(".", 1)[0] if "." in __package__ else __package__
@@ -406,7 +407,7 @@ class DLM_OT_migrator_basebody_shapekeys(Operator):
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_description = "Copy FK arm and finger rotations from original to replacement (uses constraints)"
bl_icon = "BONE_DATA"
bl_options = {"REGISTER", "UNDO"}
@@ -429,6 +430,35 @@ class DLM_OT_migrator_fk_rotations(Operator):
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, 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"}
+
+
MIGRATOR_STEP_OPS = (
"dlm.migrator_copy_attributes",
"dlm.migrator_migrate_nla",
@@ -718,4 +748,5 @@ OPERATOR_CLASSES = [
DLM_OT_tweak_remove_both,
DLM_OT_tweak_bake_both,
DLM_OT_migrator_fk_rotations,
+ DLM_OT_migrator_fk_rotations_bake,
]
diff --git a/ui/panels.py b/ui/panels.py
index 33c97d1..11795b4 100644
--- a/ui/panels.py
+++ b/ui/panels.py
@@ -93,8 +93,9 @@ class DLM_PT_main_panel(Panel):
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")
- row = box.row()
+ row = box.row(align=True)
row.operator("dlm.migrator_fk_rotations", text="MigFKRot", icon="BONE_DATA")
+ row.operator("dlm.migrator_fk_rotations_bake", text="Bake", icon="KEYFRAME")
# Tweak Tools
tweak_box = layout.box()