From 7e738fce11f4199fe648629505d0a514e174c9e0 Mon Sep 17 00:00:00 2001 From: Nathan Date: Mon, 9 Mar 2026 15:24:54 -0600 Subject: [PATCH] add FKRotBake, armature parent functionality in RetargRelatives --- ...character-migrator-workflow-integration.md | 435 +++++++++++++++++- ops/fk_rotations.py | 81 ++++ ops/migrator.py | 10 + ui/operators.py | 33 +- ui/panels.py | 3 +- 5 files changed, 557 insertions(+), 5 deletions(-) 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()