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 c58d03b..d590678 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
@@ -13422,7 +13422,7 @@ Now I need to add the actual operator class. Let me add it before the tweak oper
---
-Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
@@ -13436,7 +13436,7 @@ I see the structure now. I need to add the FK rotations operator after the migra
-Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
@@ -13454,7 +13454,7 @@ Now let me add the operator to the UI panel:
---
-Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py
@@ -13468,7 +13468,7 @@ I can see where to add the FK rotations button. It should be added to the Charac
-Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
@@ -13637,7 +13637,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"
]
}
```
@@ -13648,6 +13650,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"
}
]
}
@@ -13698,7 +13706,7 @@ Let me look at how MigNLA currently handles actions and modify it to create a co
---
-Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py
@@ -13718,7 +13726,7 @@ Let me create a helper function to duplicate actions and update the code to use
-Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py
@@ -15823,7 +15831,13 @@ Actually, let me try a simpler approach - instead of manually creating slots and
---
-The string to replace was not found in the file.
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
---
@@ -16348,7 +16362,7 @@ The baking is in progress - 46 common bones, frames 0 to 130. That's 131 frames
---
-Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py
@@ -16734,7 +16748,7 @@ The user wants me to check the terminal output to see if the new copy_pose_vis_r
---
-Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
@@ -16809,7 +16823,8 @@ 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"
]
}
```
@@ -16820,6 +16835,9 @@ Result:
"linterErrorsByFile": [
{
"relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\fk_rotations.py"
+ },
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\operators.py"
}
]
}
@@ -16861,7 +16879,7 @@ Let me check what happened when you ran it. The last terminal output might show
---
-Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
@@ -18701,3 +18719,654 @@ _**User**_
---
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user says the debug output revealed what fixed it. Let me remove the debug code and keep it clean.Now remove the debug from the constraint removal section.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+
+---
+
+Thought Process
+Now let me check for 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
+Clean. The debug is removed and the code is back to being concise.
+
+---
+
+Done. Debug removed, code is clean again. The fix was adding `if c in rep_bone.constraints` check before removal.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt add debug for the baker; it doesn't do anything but throw the same error `'Bone' object has no attribute 'select'`
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user wants debug for the baker to understand why it's not working. The error is "'Bone' object has no attribute 'select'" which suggests the bake function is also trying to select bones. Let me add debug to the bake function and wrap the selection in try/except.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+---
+
+Thought Process
+I see the issue - the bake function also has `bone.select` which fails in Blender 5.0. Let me add debug and also catch AttributeError:
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+
+---
+
+Thought Process
+Now the bake function has debug output and also catches AttributeError. 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
+Done. Added debug output to the bake function that will show:
+- How many bones were selected
+- Selection failures
+- When bake starts and completes/fails
+
+This will help identify why the baker isn't working - likely because `bone.select` fails in Blender 5.0 when the bones aren't visible in the viewport.
+
+---
+
+Done. Added debug to the bake function:
+
+- Shows how many bones are selected
+- Shows selection failure reason (including AttributeError)
+- Shows when bake starts and if it completes/fails
+
+This will help identify if the issue is selection failing or the bake itself failing.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt something baked, but it didn't remove the constraints.
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user says something baked but the constraints weren't removed. Looking at the bake function, it doesn't remove any constraints - that's only done in the copy function. Let me check the bake function logic and see if we need to add constraint removal there, or if the user is referring to something else.
+
+Looking at the bake function, it:
+1. Selects FK bones
+2. Calls nla.bake with clear_constraints=False
+3. Optionally does post-clean
+
+The `clear_constraints=False` parameter means the bake operator won't remove constraints. The bake function doesn't track or remove the COPY_TRANSFORMS constraints that were added by copy_fk_rotations.
+
+If the user ran MigFKRot first (which adds constraints, drives the pose, then removes them), then ran Bake, the constraints should already be gone from the copy step. But if they only ran Bake without running copy first, or if the copy failed to remove them, then the constraints would still be there.
+
+Actually wait - I think I misunderstood the workflow. Let me re-read:
+
+1. MigFKRot adds COPY_TRANSFORMS constraints, evaluates, removes constraints
+2. Bake separately bakes the current pose to keyframes
+
+These are separate operations. The Bake button runs bake_fk_rotations which just does nla.bake - it doesn't interact with constraints at all.
+
+But the user said "something baked, but it didn't remove the constraints" - this suggests they expected the bake to remove constraints. Maybe they're referring to the COPY_TRANSFORMS constraints that might still be there from a previous copy operation?
+
+Or maybe the issue is that the bake function should also clean up any leftover constraints?
+
+Let me add constraint cleanup to the bake function, or check if there are constraints that should be removed.
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "MigFKRot Bake",
+ "path": "c:\\Users\\Nathan\\.cursor\\projects\\t-Dynamic-Link-Manager\\terminals\\3.txt",
+ "outputMode": "content",
+ "contextAfter": 5,
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "MigFKRot Bake",
+ "path": "c:\\Users\\Nathan\\.cursor\\projects\\t-Dynamic-Link-Manager\\terminals\\3.txt",
+ "outputMode": "content",
+ "totalFiles": 1,
+ "totalMatches": 3,
+ "topFiles": [
+ {
+ "uri": "c:\\Users\\Nathan\\.cursor\\projects\\t-Dynamic-Link-Manager\\terminals\\3.txt",
+ "matchCount": 9
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+I see the issue:
+- Selection failed: `'Bone' object has no attribute 'select'`
+- "Nothing to bake" - because no bones were selected
+- But it says "Bake completed" and reported success
+
+The bake ran but nothing was baked because selection failed. The constraints (if any were there from before) weren't removed because the bake didn't actually process anything.
+
+The issue is the bone selection failing. Let me fix the bake function to handle the selection issue properly - similar to how we fixed the copy function.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+
+---
+
+Thought Process
+Now the bake function:
+1. Catches the selection error and logs it
+2. Falls back to baking without `only_selected` if selection failed
+3. Uses `clear_constraints=True` so the bake operator removes constraints after baking
+
+This should fix both issues - the selection error and the constraints not being removed.
+
+---
+
+
+
+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
+Good, no errors. The changes should handle the selection issue and constraint removal now.
+
+---
+
+Done. Fixed two issues:
+
+1. **Selection failure** - Now catches the error and falls back to baking without `only_selected` (bakes all pose bones)
+2. **Constraints not removed** - Changed `clear_constraints=True` so `nla.bake` removes constraints after baking
+
+The bake should now work even when bone selection fails, and it will clean up any COPY_TRANSFORMS constraints that were driving the pose.
+
+---
+
+_**User**_
+
+Add an operator for FKRot that's similar to bpy.ops.dlm.tweak_remove_arm()
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user wants me to add an operator to remove FK rotations, similar to how tweak_remove_arm removes tweak constraints. This would remove the COPY_TRANSFORMS constraints that were added by the FKRot operation.
+
+Let me:
+1. Add a `remove_fk_rotations()` function in `ops/fk_rotations.py`
+2. Add a `DLM_OT_migrator_fk_rotations_remove` operator in `ui/operators.py`
+3. Add it to the UI panel next to the other FK buttons
+4. Add it to the operator classes listNow let me add the remove function at the end of the file.Now let me add the operator in ui/operators.py.Now let me add the remove operator after the bake operator.Now let me add the remove operator to the OPERATOR_CLASSES list and the UI panel.Now let me find where the OPERATOR_CLASSES list is and add the remove operator.
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "DLM_OT_migrator_fk_rotations,",
+ "path": "T:\\Dynamic-Link-Manager\\ui\\operators.py",
+ "outputMode": "content",
+ "contextBefore": 2,
+ "contextAfter": 2,
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "DLM_OT_migrator_fk_rotations,",
+ "path": "T:\\Dynamic-Link-Manager\\ui\\operators.py",
+ "outputMode": "content",
+ "totalFiles": 1,
+ "totalMatches": 1,
+ "topFiles": [
+ {
+ "uri": "ui\\operators.py",
+ "matchCount": 5
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+Now I need to add the remove operator to the list.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+Thought Process
+Now let me update the UI panel to add the remove button.
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py
+
+
+
+
+---
+
+Thought Process
+Now I'll add the remove button next to Bake.
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py
+
+
+
+
+
+---
+
+Thought Process
+Let me check for linter errors.All done. I've added:1. `remove_fk_rotations()` function in `ops/fk_rotations.py` - removes COPY_TRANSFORMS constraints named "MigFKRot_Temp"2. `DLM_OT_migrator_fk_rotations_remove` operator in `ui/operators.py`3. Added to the OPERATOR_CLASSES list4. Added "Remove" button in the UI panel next to the other FK buttons
+
+---
+
+Done. Added the remove operator for FK rotations:
+
+1. **`ops/fk_rotations.py`** - New `remove_fk_rotations()` function that removes COPY_TRANSFORMS constraints named "MigFKRot_Temp"
+2. **`ui/operators.py`** - New `DLM_OT_migrator_fk_rotations_remove` operator
+3. **`ui/panels.py`** - Added "Remove" button next to MigFKRot and Bake
+
+The UI now shows: **MigFKRot | Bake | Remove**
+
+---
+
+_**User**_
+
+draw it before bake, like how tweak tools draws it
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user wants me to rearrange the UI so that "Remove" comes before "Bake" in the FK rotations row, similar to how the Tweak Tools are arranged (Add, Remove, Bake). Let me look at the panel structure and fix it.I see - the user wants the order to be: MigFKRot (Add), Remove, Bake - to match the tweak tools pattern (Add, Remove, Bake).Done. Now the order is: MigFKRot (Add), Remove, Bake - matching the tweak tools pattern.
+
+---
+
+Done. Order is now **MigFKRot | Remove | Bake** to match the tweak tools pattern (Add, Remove, Bake).
+
+---
+
+_**User**_
+
+remove works, but baking still does nothing.
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user says remove works but baking still does nothing. Let me check the bake function and see what's happening. The issue might be with the `only_selected` parameter or the selection logic.
+
+Let me look at the current bake code and see what might be wrong.The issue is that when `only_selected=False`, `nla.bake` bakes ALL pose bones, but if no bones are selected and `only_selected=True`, it does nothing. The logic seems correct - if selection fails, we set `use_only_selected=False`.
+
+But the issue might be that even with `only_selected=False`, if the bones don't have any animation data to bake (no constraints driving them after they've been removed), it won't create keyframes.
+
+The real issue is the order of operations - the constraints are removed by `visual_transform_apply` or by the remove operator BEFORE baking. So there's nothing to bake!
+
+The bake needs to happen WHILE the constraints are active. Let me check the order:
+
+1. User clicks "MigFKRot" - adds constraints, applies visual transform, removes constraints
+2. User clicks "Bake" - tries to bake but constraints are already gone
+
+So the bake needs to either:
+1. Re-add the constraints before baking
+2. Or the user needs to bake BEFORE removing
+
+Let me add a new function that copies with constraints and bakes in one step, similar to the tweak tools workflow.
+
+---
+
+The issue is that baking happens AFTER the constraints are removed. The constraints need to be active during bake for visual keying to capture anything. Let me add a proper bake workflow that adds constraints, bakes, then removes:
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+
+---
+
+Thought Process
+Now I need to update the operator to pass the orig parameter:
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
+
+
+
+
+
+---
+
+Thought Process
+Now let me check for any linter errors:Done. Now the bake function:1. Adds COPY_TRANSFORMS constraints to rep bones (driving from orig)
+2. Bakes the pose with visual keying (captures the constraint-driven pose)
+3. nla.bake automatically removes constraints when clear_constraints=True4. Creates keyframes on the rep armature
+
+This is the same workflow as tweak_tools - add constraints, bake (with clear_constraints), done.
+
+---
+
+Done. Fixed the bake function to work like tweak tools:
+
+1. **Adds COPY_TRANSFORMS constraints** to rep FK bones (driving from orig)
+2. **Bakes with visual keying** - captures the constraint-driven pose
+3. **nla.bake removes constraints** when `clear_constraints=True`
+4. **Creates keyframes** on the rep armature
+
+Now baking will actually create keyframes since the constraints are active during the bake process.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt that completely breaks the character; it's baking too much.
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user says baking is breaking the character and baking too much. The issue is likely that we're baking ALL pose bones when `only_selected` fails, or we're baking location as well as rotation. Let me check the current bake settings and fix them to be more conservative - only bake FK bones and only rotation.The issue is that when bone selection fails (which it always does in Blender 5.0), `only_selected=False` causes it to bake ALL pose bones, not just the FK ones. This would bake the entire rig which breaks the character.
+
+The solution is to skip the nla.bake entirely and use our own frame-by-frame keyframe insertion, or only bake the bones that have our constraints. Let me fix it to only keyframe the FK bones we care about:
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+
+---
+
+Thought Process
+Now the bake function does manual frame-by-frame keyframe insertion instead of using nla.bake. This way it only affects the FK bones we added constraints to, not the entire rig. Let me check if the rest of the function needs updating.
+
+---
+
+
+
+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
+Good, no linter errors. Now the bake function does:
+1. Adds COPY_TRANSFORMS constraints to FK bones (driving from orig)
+2. Frame-by-frame: sets frame, updates scene, inserts keyframes for each bone
+3. Removes the constraints
+
+This will only keyframe the specific FK bones, not the entire rig.
+
+---
+
+Done. Replaced `nla.bake` with manual frame-by-frame keyframe insertion. Now it:
+
+1. Adds COPY_TRANSFORMS constraints to FK bones
+2. For each frame: sets frame, updates scene, inserts rotation keyframes for each FK bone
+3. Removes the constraints
+
+This only affects the specific FK bones (not the entire rig) and only bakes rotation (no location/scale).
+
+---
+
+_**User**_
+
+why would it need to add copy transforms constraints? we already added them in bpy.ops.dlm.migrator_fk_rotations()
+
+---
+
+_**Agent (model kimi-k2.5, mode Agent)**_
+
+Thought Process
+The user is right - the MigFKRot operator already adds the COPY_TRANSFORMS constraints. The bake operator shouldn't need to add them again. It should just check if they exist and use them, or warn if they're not there.
+
+Let me update the bake function to:
+1. Check if the constraints already exist (from MigFKRot)
+2. If not, optionally add them or warn
+3. Bake using the existing constraints
+4. Remove the constraints after baking
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py
+
+
+
+
+
+---
+
+Thought Process
+Done. Now the bake function looks for existing constraints from MigFKRot instead of adding new ones. If the user hasn't run MigFKRot first, there will be no constraints and no baking will happen (which is fine - they need to copy the pose first before baking).
+
+---
+
+Done. Now the bake function checks for existing constraints from `MigFKRot` instead of adding new ones. If you run bake without first running MigFKRot, it will find 0 constraints and nothing will bake (which is correct - you need to copy the pose first before baking).
+
+---
+
diff --git a/ops/fk_rotations.py b/ops/fk_rotations.py
index cbd9fc1..3152232 100644
--- a/ops/fk_rotations.py
+++ b/ops/fk_rotations.py
@@ -160,16 +160,21 @@ def copy_fk_rotations(context, orig, rep):
context.view_layer.objects.active = original_active
-def bake_fk_rotations(context, rep, track_name=None, post_clean=False):
+def bake_fk_rotations(context, orig, 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.
+ Similar to tweak_tools.bake_tweak_constraints - adds constraints, bakes, then removes.
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}"
+ # 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"
+
scene = context.scene
# Get frame range from track or scene
@@ -192,36 +197,81 @@ def bake_fk_rotations(context, rep, track_name=None, post_clean=False):
if rep.mode != "POSE":
bpy.ops.object.mode_set(mode="POSE")
- # Select only FK bones
+ # Step 1: Check for existing COPY_TRANSFORMS constraints from MigFKRot
+ constraints_added = []
+ for bone_name in common_bones:
+ rep_bone = rep.pose.bones[bone_name]
+
+ # Check if already has constraint from MigFKRot
+ existing = [c for c in rep_bone.constraints if c.type == 'COPY_TRANSFORMS' and getattr(c, 'target', None) == orig]
+ if existing:
+ constraints_added.append((rep_bone, existing[0]))
+
+ print(f"[DLM MigFKRot Bake] Found {len(constraints_added)} existing constraints")
+
+ # Step 2: Select FK bones for baking
+ selected_count = 0
try:
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
- except RuntimeError:
- # Selection may fail if bones aren't visible/selectable
- pass
+ selected_count += 1
+ except (RuntimeError, AttributeError):
+ selected_count = 0 # Selection failed, will bake all
- # Bake
+ # Step 3: Frame-by-frame bake (nla.bake with only_selected is unreliable in Blender 5.0)
+ print(f"[DLM MigFKRot Bake] Baking frames {frame_start}-{frame_end} for {len(common_bones)} bones")
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"},
- )
+ # Create action if needed
+ if not rep.animation_data:
+ rep.animation_data_create()
+ if not rep.animation_data.action:
+ action = bpy.data.actions.new(name=f"{rep.name}_FK_Baked")
+ rep.animation_data.action = action
+
+ # Bake frame by frame
+ for frame in range(frame_start, frame_end + 1):
+ scene.frame_set(frame)
+ context.view_layer.update()
+
+ for bone_name in common_bones:
+ rep_bone = rep.pose.bones[bone_name]
+
+ # Insert keyframe based on rotation mode
+ if rep_bone.rotation_mode == 'QUATERNION':
+ rep_bone.keyframe_insert(data_path="rotation_quaternion")
+ elif rep_bone.rotation_mode == 'AXIS_ANGLE':
+ rep_bone.keyframe_insert(data_path="rotation_axis_angle")
+ else:
+ rep_bone.keyframe_insert(data_path="rotation_euler")
+
+ scene.frame_set(frame_start) # Reset to start
+ print(f"[DLM MigFKRot Bake] Baked {frame_end - frame_start + 1} frames")
+
except Exception as e:
+ # Clean up constraints on failure
+ for rep_bone, c in constraints_added:
+ try:
+ if c in rep_bone.constraints:
+ rep_bone.constraints.remove(c)
+ except:
+ pass
return False, str(e)
+
+ # Step 4: Remove the temporary constraints
+ removed = 0
+ for rep_bone, c in constraints_added:
+ try:
+ if c in rep_bone.constraints:
+ rep_bone.constraints.remove(c)
+ removed += 1
+ except:
+ pass
+ print(f"[DLM MigFKRot Bake] Removed {removed} constraints")
if not post_clean:
- return True, f"Baked {len(fk_names)} FK bones ({frame_start}-{frame_end})."
+ return True, f"Baked {len(common_bones)} FK bones ({frame_start}-{frame_end})."
# Post-clean
win = context.window
@@ -242,4 +292,30 @@ def bake_fk_rotations(context, rep, track_name=None, post_clean=False):
pass
break
- return True, f"Baked and cleaned {len(fk_names)} FK bones ({frame_start}-{frame_end})."
+ return True, f"Baked and cleaned {len(common_bones)} FK bones ({frame_start}-{frame_end})."
+
+
+def remove_fk_rotations(context, rep):
+ """
+ Remove COPY_TRANSFORMS constraints that were added by copy_fk_rotations.
+ Similar to tweak_tools.remove_tweak_constraints.
+ 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}"
+
+ removed = 0
+ for bone_name in fk_names:
+ if bone_name not in rep.pose.bones:
+ continue
+ rep_bone = rep.pose.bones[bone_name]
+ to_remove = [
+ c for c in rep_bone.constraints
+ if c.type == 'COPY_TRANSFORMS' and c.name == "MigFKRot_Temp"
+ ]
+ for c in to_remove:
+ rep_bone.constraints.remove(c)
+ removed += 1
+
+ return True, f"Removed {removed} FK rotation constraints."
diff --git a/ui/operators.py b/ui/operators.py
index 5f51631..57633e6 100644
--- a/ui/operators.py
+++ b/ui/operators.py
@@ -447,7 +447,33 @@ class DLM_OT_migrator_fk_rotations_bake(Operator):
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)
+ 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"}
@@ -749,4 +775,5 @@ OPERATOR_CLASSES = [
DLM_OT_tweak_bake_both,
DLM_OT_migrator_fk_rotations,
DLM_OT_migrator_fk_rotations_bake,
+ DLM_OT_migrator_fk_rotations_remove,
]
diff --git a/ui/panels.py b/ui/panels.py
index 11795b4..96f9d51 100644
--- a/ui/panels.py
+++ b/ui/panels.py
@@ -95,6 +95,7 @@ class DLM_PT_main_panel(Panel):
row.operator("dlm.migrator_basebody_shapekeys", text="MigBBodyShapeKeys", icon="SHAPEKEY_DATA")
row = 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