add FKRotBake, armature parent functionality in RetargRelatives

This commit is contained in:
Nathan
2026-03-09 15:24:54 -06:00
parent 440b206116
commit 7e738fce11
5 changed files with 557 additions and 5 deletions
@@ -14395,7 +14395,7 @@ Let me update the fk_rotations.py to implement this baking approach.</details></
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py</summary>
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
</details>
</tool-use>
@@ -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 data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
@@ -17241,3 +17255,418 @@ The constraints are driving the pose but not "baking" it. When removed, the bone
---
<think><details><summary>Thought Process</summary>
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.</details></think>
---
<tool-use data-tool-type="unknown" data-tool-name="read_lints">
<details>
<summary>Tool use: **read_lints**</summary>
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**
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
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.</details></think>
---
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)**_
<think><details><summary>Thought Process</summary>
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.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\tweak_tools.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
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.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\fk_rotations.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now I'll add a bake function similar to the tweak tools. Let me append it to the fk_rotations.py file.</details></think>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\fk_rotations.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now I need to add the UI operator and register it. Let me add a new operator class in ui/operators.py.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now I need to add the bake operator. Let me insert it after the existing MigFKRot operator.</details></think>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now I need to register the new operator and add it to the UI. Let me find the registration section and add it there.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now I need to add the new operator to the OPERATOR_CLASSES list.</details></think>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
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.</details></think>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
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)</details></think>
---
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)**_
<think><details><summary>Thought Process</summary>
The user can't enable the addon. Let me check the terminal for errors.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
There's a `enableFailure` message. Let me check if there's more context about what caused it. Let me read more lines.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
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.</details></think>
---
<tool-use data-tool-type="unknown" data-tool-name="ripgrep_raw_search">
<details>
<summary>Tool use: **ripgrep_raw_search**</summary>
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**
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
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`.</details></think>
---
Fixed. Just needed to import `BoolProperty` and `StringProperty` from `bpy.props`.
---
<tool-use data-tool-type="generic" data-tool-name="read_lints">
<details>
<summary>Tool use: **read_lints** • Read lints for 1 file</summary>
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 |
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
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.</details></think>
---
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)**_
<think><details><summary>Thought Process</summary>
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.</details></think>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I need to add logic to replicate the parent of `orig` on `rep`. If `orig.parent` exists, set `rep.parent = orig.parent`.</details></think>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Done. Now if the original armature has a parent, the replacement will also be parented to the same object/bone.</details></think>
---
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 data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py</summary>
Status: **loading**
</details>
</tool-use>
---
---
---
+81
View File
@@ -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})."
+10
View File
@@ -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:
+32 -1
View File
@@ -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,
]
+2 -1
View File
@@ -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()