From 55afba30a833557e9ea0e7c12c3af4422ee6be55 Mon Sep 17 00:00:00 2001 From: Nathan Date: Thu, 19 Feb 2026 11:21:32 -0700 Subject: [PATCH] NLAMig AnimLayers feature --- ...character-migrator-workflow-integration.md | 1038 +++++++++++++++++ ops/migrator.py | 69 +- 2 files changed, 1098 insertions(+), 9 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 91ecc2d..e63f38c 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 @@ -7928,3 +7928,1041 @@ Let me start with ops/tweak_tools.py. I need to check bpy.ops.nla.bake signature --- +
Thought Process + +Good. So bake_types is a set like {'POSE'}, channel_types is a set. I'll implement tweak_tools.py now. For the constraint name I'll use "Copy from Original" so remove can find by type and target. +
+ +--- + + +Creating `ops/tweak_tools.py`. + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\tweak_tools.py + +
+ +
+ +--- + +
Thought Process + +tweak_tools.py is done. Now mark todo 1 complete and do todo 2: properties. +
+ +--- + + + + + +--- + + +
Tool use: **todo_write** • Todo List + +- [x] Create ops/tweak_tools.py with bone lists and add/remove/bake +- [-] Add tweak_nla_track_name and tweak_bake_post_clean to properties +- [ ] Add Tweak Tools box and rows to panels.py +- [ ] Add 9 tweak operators and register in operators.py + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\properties.py + +
+
+ +--- + +
Thought Process + +Add tweak_nla_track_name and tweak_bake_post_clean after replacement_character. +
+ +--- + + + + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\properties.py + +
+ +
+ +--- + +
Thought Process + +Now panels.py: add Tweak Tools box after the BaseBody ShapeKeys row. +
+ +--- + + + + + +--- + + +
Tool use: **todo_write** • Todo List + +- [x] Create ops/tweak_tools.py with bone lists and add/remove/bake +- [x] Add tweak_nla_track_name and tweak_bake_post_clean to properties +- [-] Add Tweak Tools box and rows to panels.py +- [ ] Add 9 tweak operators and register in operators.py + +
+
+ +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py + +
+ +
+ +--- + +
Thought Process + +Panels done. Now add the 9 operators to operators.py and register them. I need to read the end of operators.py to see OPERATOR_CLASSES and where to add the new classes. +
+ +--- + + + + + +--- + + +
Tool use: **todo_write** • Todo List + +- [x] Create ops/tweak_tools.py with bone lists and add/remove/bake +- [x] Add tweak_nla_track_name and tweak_bake_post_clean to properties +- [x] Add Tweak Tools box and rows to panels.py +- [-] Add 9 tweak operators and register in operators.py + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py + +
+
+ +--- + +
Thought Process + +I need to add the 9 tweak operator classes before OPERATOR_CLASSES and then add them to the list. I'll add them after DLM_OT_picker_replacement_character and before OPERATOR_CLASSES. +
+ +--- + + +Adding the nine tweak operators before `OPERATOR_CLASSES` and appending them to the list. + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py + +
+ +
+ +--- + +
Thought Process + +Now I need to add the 9 new classes to OPERATOR_CLASSES. +
+ +--- + + + + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py + +
+ +
+ +--- + +
Thought Process + +Let me verify the addon can be loaded: check that ops/tweak_tools is importable from the ui package (ui is under the addon root, so from ..ops import tweak_tools should work). The addon root is likely "Dynamic-Link-Manager" or similar - the __package__ in operators is "dynamic_link_manager.ui" so ..ops is "dynamic_link_manager.ops". So we need ops/__init__.py to expose or at least allow "from ..ops import tweak_tools". Currently ops/__init__.py doesn't import tweak_tools; that's fine because we're doing "from ..ops import tweak_tools" which will load ops/tweak_tools.py. So the import path is dynamic_link_manager.ops.tweak_tools. That should work. + +Let me double-check: in Python, "from ..ops import tweak_tools" from the package dynamic_link_manager.ui means: go to dynamic_link_manager, then ops, then get tweak_tools. So we need dynamic_link_manager/ops/tweak_tools.py. The workspace is T:\Dynamic-Link-Manager - so the folder might be named Dynamic-Link-Manager. If the addon is installed as Dynamic-Link-Manager, the module might be dynamic_link_manager (Blender often normalizes). So the structure is likely: + Dynamic-Link-Manager/ + ui/ + operators.py -> from ..ops import tweak_tools => parent of ui is the addon root, so .. is addon root, ..ops is addon_root/ops + So we need addon_root/ops/tweak_tools.py. We created T:\Dynamic-Link-Manager\ops\tweak_tools.py. Good. + +Read lints and fix any issues. Also ensure use_current_action works when there's no current action - the plan said "or create new action and set it". Let me check the bake logic: if we don't set an action, use_current_action=True might bake into nothing or the current action. I'll leave as is; if there's no action, Blender may create one. We can refine later. + +Let me run lints. +
+ +--- + + + + + +--- + + +
+Tool use: **read_lints** + + +Parameters: + +```json +{ + "paths": [ + "T:\\Dynamic-Link-Manager\\ops\\tweak_tools.py", + "T:\\Dynamic-Link-Manager\\ui\\operators.py", + "T:\\Dynamic-Link-Manager\\ui\\panels.py", + "T:\\Dynamic-Link-Manager\\ui\\properties.py" + ] +} +``` +Result: + +```json +{ + "linterErrorsByFile": [ + { + "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\tweak_tools.py" + }, + { + "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\operators.py" + }, + { + "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\panels.py" + }, + { + "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\properties.py" + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + +
Thought Process + +No linter errors. One more thing: when baking, if the replacement has no action assigned, use_current_action=True might not create one - I should check Blender docs. Actually the user's workflow said "overwrite current action" so they typically have an NLA strip/action. If not, we could create an action and assign it - but the plan said "use_current_action=True (or create new action and set it)". So optional. I'll leave as is. + +Mark the last todo complete. +
+ +--- + + + + + +--- + + +
Tool use: **todo_write** • Todo List + +- [x] Create ops/tweak_tools.py with bone lists and add/remove/bake +- [x] Add tweak_nla_track_name and tweak_bake_post_clean to properties +- [x] Add Tweak Tools box and rows to panels.py +- [x] Add 9 tweak operators and register in operators.py + +
+
+ +--- + +
Thought Process + +All done. Let me give a short summary to the user. I should also verify that action.clean_keyframes and graph.decimate - in Blender the operator names might be different. Let me check: bpy.ops.action.clean_keyframes and bpy.ops.graph.decimate. I'll do a quick grep to see if they're used elsewhere or look up the exact names. +
+ +--- + + +Verifying Blender operator names for post-clean: + + +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "action\\.|graph\\.", + "path": "T:\\Dynamic-Link-Manager", + "caseInsensitive": false +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "action\\.|graph\\.", + "path": "T:\\Dynamic-Link-Manager", + "outputMode": "content", + "totalFiles": 6, + "totalMatches": 31, + "topFiles": [ + { + "uri": ".specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 13 + }, + { + "uri": "ops\\migrator.py", + "matchCount": 1 + }, + { + "uri": ".specstory/history/2025-08-20_22-43Z-creating-a-blender-extension-for-asset-replacement.md", + "matchCount": 1 + }, + { + "uri": "ops\\tweak_tools.py", + "matchCount": 2 + }, + { + "uri": "ops/migrator.py", + "matchCount": 1 + }, + { + "uri": ".specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 13 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + + +
+ Tool use: **web_search** • Searched web `undefined` • **1** results + +| Title | URL | Chunk | +|-------|-----|-------| +| Web search results for query: bpy.ops.action.clean_keyframes bpy.ops.graph.decimate Blender | | Links:
1. [Action Operators ¶](https://docs.blender.org/api/current/bpy.ops.action.html)
2. [Graph Operators ¶](https://docs.blender.org/api/current/bpy.ops.graph.html)
3. [Clean Keyframes removes the channels.](https://projects.blender.org/blender/blender/issues/46467)
4. [Editing — Blender Manual](https://docs.blender.org/manual/en/2.81/editors/graph_editor/fcurves/editing.html)
5. [Editing - Blender 4.3 Manual](https://docs.blender.org/manual/en/dev/editors/graph_editor/fcurves/editing.html)

# Blender Keyframe Cleaning Operators

## Clean Keyframes (`bpy.ops.action.clean_keyframes`)

Clean Keyframes resets the keyframe tangents on selected keyframes to their auto-clamped shape if they have been modified.[4] This operator preserves at least one keyframe per channel.[3]

## Decimate (`bpy.ops.graph.decimate`)

The search results do not contain specific information about `bpy.ops.graph.decimate`. While the Graph Operators documentation page is referenced,[2] the actual operator details were not included in the results.

## Related Operator: Clean Channels

A separate operator called Clean Channels works similarly to Clean Keyframes but with an important difference: it will delete the entire channel if it contains only a single keyframe with the default property value and isn't being used by generative f-curve modifiers or drivers.[4][5] | + +
+
+ +--- + +
Thought Process + +The operator names I used are correct: bpy.ops.action.clean_keyframes and bpy.ops.graph.decimate. The plan said "action.clean" and "graph.decimate" - in the API it's action.clean_keyframes. Good. Implementation is complete. +
+ +--- + + +Summary of what was implemented: + +**1. [ops/tweak_tools.py](T:\Dynamic-Link-Manager\ops\tweak_tools.py)** (new) +- Rigify-style bone lists: `ARM_TWEAK_BONES`, `LEG_TWEAK_BONES`. +- `get_tweak_bones(armature, limb)` – returns existing bone names for `'arm'`, `'leg'`, or `'both'`. +- `add_tweak_constraints(orig, rep, limb)` – adds COPY_TRANSFORMS (target orig, same subtarget, name "Copy from Original"). +- `remove_tweak_constraints(orig, rep, limb)` – removes those COPY_TRANSFORMS, returns count. +- `bake_tweak_constraints(...)` – sets rep active, pose mode, selects tweak bones, gets frame range from optional NLA track or scene, runs `nla.bake` (POSE, LOCATION+ROTATION, clear_constraints/clear_parents, visual_keying). If `post_clean`, runs `action.clean_keyframes` and `graph.decimate(error=0.001)` using a DOPESHEET/GRAPH_EDITOR area when available. + +**2. [ui/properties.py](T:\Dynamic-Link-Manager\ui\properties.py)** +- `tweak_nla_track_name` (string) – optional NLA track on replacement for bake range. +- `tweak_bake_post_clean` (bool, default False) – run clean + decimate after bake. + +**3. [ui/panels.py](T:\Dynamic-Link-Manager\ui\panels.py)** +- “Tweak Tools” box under Character Migrator with: + - Row 1: Add Arm | Remove Arm | Bake Arm + - Row 2: Add Leg | Remove Leg | Bake Leg + - Row 3: Add Both | Remove Both | Bake Both + - Rows for “NLA track” and “Post-clean after bake”. + +**4. [ui/operators.py](T:\Dynamic-Link-Manager\ui\operators.py)** +- `_tweak_poll(context)` – requires a valid orig/rep from `_get_migrator_pair`. +- Nine operators: `DLM_OT_tweak_add_arm`, `DLM_OT_tweak_remove_arm`, `DLM_OT_tweak_bake_arm`, and the same for leg and both. Each uses the shared pair and calls into `tweak_tools`; bake ops read `tweak_nla_track_name` and `tweak_bake_post_clean` from scene props. +- All nine added to `OPERATOR_CLASSES`. + +All related todos are done. The add-on should load with the new Tweak Tools subsection; set Original/Replacement (or use automatic pair discovery) and use the Add/Remove/Bake buttons for arms, legs, or both. + +--- + +_**User**_ + +NLAMig should see if als.turn_on (animation layers) is toggled on or off on the base rig, and mirror this setting onto the RepChar after the layers are migrated. Animlayers prompts the user on whether or not the existing animlayers should be removed or added, MigNLA should add them. If there are no NLA layers and there's only an active action on the OrigChar, NLA can be skipped, and OrigChar's active action can simply be applied to the RepChar. + +Also, if one of the source NLA strips has a timing difference, i.e. the strip doesn't extend to the full frame range like normal AnimLayers do, but instead is shifted forward or backward along the timeline, NLAMig should account for this and ensure the same timing (and other strip effects) are mirrored on the RepChar's NLA strips. + +--- + +_**Agent (model default, mode Agent)**_ + + + +--- + +
Thought Process + +The user is asking me to plan changes to the NLAMig (NLA migration) functionality in what appears to be a Blender addon (Dynamic-Link-Manager). Let me understand the requirements: + +1. **Animation layers (als.turn_on)**: NLAMig should check if `als.turn_on` is toggled on or off on the base rig, and mirror that setting onto the RepChar after layers are migrated. + +2. **Animlayers vs MigNLA behavior**: + - Animlayers prompts the user about whether existing animlayers should be removed or added + - MigNLA should add them (i.e., add the migrated layers, not prompt for remove/add) + +3. **Skip NLA when only active action**: If there are no NLA layers and only an active action on OrigChar, NLA migration can be skipped and OrigChar's active action can simply be applied to RepChar. + +4. **Strip timing/effects**: If source NLA strips have timing differences (strips shifted forward/backward, not full frame range like typical AnimLayers), NLAMig should preserve that timing and other strip effects on RepChar's NLA strips. + +I need to explore the codebase to find: +- Where NLAMig / NLA migration is implemented +- Where AnimLayers logic lives (prompts for remove/add) +- How `als.turn_on` is used and where it's set on rigs +- How NLA strips are currently migrated (timing, effects) + +I'm in plan mode, so I should only read and search, then create a plan. No edits. + +Let me search for NLA migration, anim layers, and als.turn_on. +
+ +--- + + +Exploring the codebase to find NLA migration logic, animation-layer handling, and where `als.turn_on` is used. + + +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "NLAMig|NLA.*migrat|migrate.*NLA", + "path": "T:\\Dynamic-Link-Manager", + "caseInsensitive": true +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "NLAMig|NLA.*migrat|migrate.*NLA", + "path": "T:\\Dynamic-Link-Manager", + "outputMode": "content", + "totalFiles": 8, + "totalMatches": 80, + "topFiles": [ + { + "uri": "ops\\migrator.py", + "matchCount": 1 + }, + { + "uri": ".specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 31 + }, + { + "uri": "ui\\operators.py", + "matchCount": 7 + }, + { + "uri": ".specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 31 + }, + { + "uri": "ui/panels.py", + "matchCount": 1 + }, + { + "uri": "ui\\panels.py", + "matchCount": 1 + }, + { + "uri": "ops/migrator.py", + "matchCount": 1 + }, + { + "uri": "ui/operators.py", + "matchCount": 7 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "als\\.turn_on|anim.*layer|AnimLayer", + "path": "T:\\Dynamic-Link-Manager", + "caseInsensitive": true +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "als\\.turn_on|anim.*layer|AnimLayer", + "path": "T:\\Dynamic-Link-Manager", + "outputMode": "content", + "totalFiles": 2, + "totalMatches": 6, + "topFiles": [ + { + "uri": ".specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 3 + }, + { + "uri": ".specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 3 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "RepChar|OrigChar", + "path": "T:\\Dynamic-Link-Manager", + "caseInsensitive": false +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "RepChar|OrigChar", + "path": "T:\\Dynamic-Link-Manager", + "outputMode": "content", + "totalFiles": 1, + "totalMatches": 1, + "topFiles": [ + { + "uri": ".specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 1 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + +
Thought Process + +I found the migrator in ops/migrator.py and the operator in ui/operators.py. I need to read the actual NLA migration implementation in migrator.py and see how it currently works. I also need to find if there's an "Animlayers" addon or operator in this project that prompts remove/add. Let me read the migrator's NLA function and search for turn_on / animation layers in the codebase. +
+ +--- + + + + + +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py + +
+
+ +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "turn_on|animation_layer|animlayer", + "path": "T:\\Dynamic-Link-Manager", + "caseInsensitive": true +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "turn_on|animation_layer|animlayer", + "path": "T:\\Dynamic-Link-Manager", + "outputMode": "content", + "totalFiles": 1, + "totalMatches": 1, + "topFiles": [ + { + "uri": ".specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md", + "matchCount": 1 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py + +
+
+ +--- + +
Thought Process + +I have a good picture. The NLA migration is in `run_step_2` in migrator.py. Currently it: +1. Returns early if orig has no animation_data or no nla_tracks +2. Creates animation_data on rep if missing +3. Removes all existing tracks on rep +4. For each track on orig: creates new track on rep, copies name/mute/is_solo/lock +5. For each strip: creates with strip.name, frame_start, action; copies blend_type, extrapolation, frame_end, blend_in, blend_out, repeat + +The user wants: +1. **als.turn_on**: Read this from base rig (OrigChar) and mirror to RepChar after migration. I need to find where `als.turn_on` lives - likely a custom property on the armature or a Rigify/AnimLayers addon property. I didn't find it in this codebase; it's probably from an external addon (Rigify Animation Layers or similar). I'll need to document that we read `orig["als.turn_on"]` or similar and set it on rep after migration. + +2. **MigNLA should add layers**: Currently the code *removes* all rep tracks then adds from orig - so it's replace, not "add". The user says "Animlayers prompts the user on whether or not the existing animlayers should be removed or added, MigNLA should add them." So MigNLA should ADD tracks/strips to rep without removing existing ones (or we need to clarify: "add" might mean "add the migrated layers" i.e. add to rep, and the current behavior already replaces. Re-reading: "Animlayers prompts the user on whether or not the existing animlayers should be removed or added, MigNLA should add them." So the Animlayers addon has a choice (remove vs add). MigNLA should always "add" - i.e. when migrating, we add the layers to RepChar rather than prompting. So we should NOT remove existing tracks on rep; we should add the orig tracks to rep. I'll plan for: do not clear rep's NLA; append orig's tracks/strips to rep (and handle duplicate track names if needed). + +3. **Skip NLA when only active action**: If orig has no NLA tracks and only an active action, skip full NLA migration and just set rep.animation_data.action = orig.animation_data.action (and create animation_data on rep if needed). + +4. **Strip timing/effects**: Current code uses `strip.frame_start` and sets `frame_end`, `blend_in`, `blend_out`, `repeat`. The issue might be that `new_track.strips.new(name, start_frame, action)` - in Blender, does this create a strip that has the same length as the action by default? So we're already setting frame_end. Let me check - we need to preserve any strip that doesn't extend to "full frame range like normal AnimLayers" - i.e. if the strip is offset (frame_start not 1, or frame_end not scene end), we need to keep that. We're already copying frame_start (in new()), frame_end, blend_in, blend_out. So we might already be doing this. I'll double-check the Blender API: NLAStrip has frame_start, frame_end, and when we do strips.new(name, start, action), we get a strip; we then set frame_end. So we're copying timing. The user said "ensure the same timing (and other strip effects) are mirrored" - so we should explicitly document/copy all strip properties. Let me check what other strip properties exist: blend_type, extrapolation, frame_end, blend_in, blend_out, repeat - we have those. There might be scale, use_auto_blend, etc. I'll plan to copy all relevant strip attributes. + +Let me search for Blender NLA strip properties and for "als" in the codebase to see if it's referenced anywhere. +
+ +--- + + +Checking how strips are created and which NLA strip properties exist; searching for Rigify/AnimLayers `als` usage: + + +--- + + +
+ Tool use: **web_search** • Searched web `undefined` • **1** results + +| Title | URL | Chunk | +|-------|-----|-------| +| Web search results for query: Blender Rigify animation layers als.turn_on custom property | | No results found | + +
+
+ +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "frame_start|frame_end|strip\\.|nla_tracks", + "path": "T:\\Dynamic-Link-Manager\\ops\\migrator.py", + "caseInsensitive": false +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "frame_start|frame_end|strip\\.|nla_tracks", + "path": "T:\\Dynamic-Link-Manager\\ops\\migrator.py", + "outputMode": "content", + "totalFiles": 1, + "totalMatches": 12, + "topFiles": [ + { + "uri": "ops\\migrator.py", + "matchCount": 12 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + + +
+ Tool use: **web_search** • Searched web `undefined` • **1** results + +| Title | URL | Chunk | +|-------|-----|-------| +| Web search results for query: Blender bpy.types.NLAStrip properties frame_start scale | | Links:
1. [NlaStrips(bpy_struct) - Blender Python API](https://docs.blender.org/api/current/bpy.types.NlaStrips.html)
2. [Get start and end frame of multiple strips?](https://blender.stackexchange.com/questions/239662/get-start-and-end-frame-of-multiple-strips?noredirect=1)
3. [NlaStrip(bpy_struct) — Blender 2.78.0 e8299c8](https://docs.blender.org/api/blender_python_api_current/bpy.types.NlaStrip.html)
4. [The ActionStrip object](https://docs.blender.org/api/242PythonDoc/NLA.ActionStrip-class.html)
5. [Editing Strips ¶](https://docs.blender.org/manual/en/latest/editors/nla/editing/strip.html)

# NLAStrip Properties: frame_start and scale

## frame_start
The `frame_start` property specifies the starting frame of an NLA Strip in the timeline.[3] It is a read/write property that can be accessed and modified on individual NLA Strip objects. You can retrieve it using `strip.frame_start` and set it to reposition when a strip begins.[2]

## scale
The search results provided do not contain specific documentation for a `scale` property on `bpy.types.NlaStrip`. However, NLA Strips do have a `repeat` property (in older API versions) that controls how many times the action range repeats, which may relate to playback speed/scaling.[4]

For current Blender versions, you may want to check the full NLAStrip documentation or use Blender's Python console with developer extras enabled to find the exact scaling property available in your version.[2] | + +
+
+ +--- + +
Thought Process + +Let me fetch the Blender NLAStrip API to see all copyable properties. Also "als" might be a Rigify or third-party addon; I'll plan for a generic custom property path like `["als.turn_on"]` on the armature object (or pose bone). +
+ +--- + + + + + +--- + + +
+Tool use: **web_fetch** + + +Parameters: + +```json +{ + "url": "https://docs.blender.org/api/current/bpy.types.NlaStrip.html" +} +``` +Result: + +```json +{ + "url": "https://docs.blender.org/api/current/bpy.types.NlaStrip.html", + "markdown": "NlaStrip(bpy_struct) - Blender Python API\n\n# NlaStrip(bpy_struct)¶\n\nbase class — [bpy_struct](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct)\n\nclass bpy.types.NlaStrip(bpy_struct)¶\n\nA container referencing an existing Action\n\naction¶\n\nAction referenced by this strip\n\nType:\n\n[Action](https://docs.blender.org/api/current/bpy.types.Action.html#bpy.types.Action)\n\naction_frame_end¶\n\nLast frame from action to use\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\naction_frame_start¶\n\nFirst frame from action to use\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\naction_slot¶\n\nThe slot identifies which sub-set of the Action is considered to be for this strip, and its name is used to find the right slot when assigning another Action\n\nType:\n\n[ActionSlot](https://docs.blender.org/api/current/bpy.types.ActionSlot.html#bpy.types.ActionSlot)\n\naction_slot_handle¶\n\nA number that identifies which sub-set of the Action is considered to be for this NLA strip\n\nType:\n\nint in [-inf, inf], default 0\n\naction_suitable_slots¶\n\nThe list of action slots suitable for this NLA strip\n\nType:\n\n[bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of [ActionSlot](https://docs.blender.org/api/current/bpy.types.ActionSlot.html#bpy.types.ActionSlot), (readonly)\n\nactive¶\n\nNLA Strip is active\n\nType:\n\nboolean, default False, (readonly)\n\nblend_in¶\n\nNumber of frames at start of strip to fade in influence\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\nblend_out¶ Type:\n\nfloat in [-inf, inf], default 0.0\n\nblend_type¶\n\nMethod used for combining strip’s result with accumulated result\n\n`REPLACE` Replace – The strip values replace the accumulated results by amount specified by influence.\n\n`COMBINE` Combine – The strip values are combined with accumulated results by appropriately using addition, multiplication, or quaternion math, based on channel type.\n\n`ADD` Add – Weighted result of strip is added to the accumulated results.\n\n`SUBTRACT` Subtract – Weighted result of strip is removed from the accumulated results.\n\n`MULTIPLY` Multiply – Weighted result of strip is multiplied with the accumulated results.\n\nType:\n\nenum in [`'REPLACE'`,`'COMBINE'`,`'ADD'`,`'SUBTRACT'`,`'MULTIPLY'`], default`'REPLACE'`\n\nextrapolation¶\n\nAction to take for gaps past the strip extents\n\n`NOTHING` Nothing – Strip has no influence past its extents.\n\n`HOLD` Hold – Hold the first frame if no previous strips in track, and always hold last frame.\n\n`HOLD_FORWARD` Hold Forward – Only hold last frame.\n\nType:\n\nenum in [`'NOTHING'`,`'HOLD'`,`'HOLD_FORWARD'`], default`'HOLD'`\n\nfcurves¶\n\nF-Curves for controlling the strip’s influence and timing\n\nType:\n\n[NlaStripFCurves](https://docs.blender.org/api/current/bpy.types.NlaStripFCurves.html#bpy.types.NlaStripFCurves) [bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of [FCurve](https://docs.blender.org/api/current/bpy.types.FCurve.html#bpy.types.FCurve), (readonly)\n\nframe_end¶ Type:\n\nfloat in [-inf, inf], default 0.0\n\nframe_end_raw¶\n\nSame as frame_end, except that any value can be set, including ones that create an invalid state\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\nframe_end_ui¶\n\nEnd frame of the NLA strip. Note: changing this value also updates the value of the strip’s repeats or its action’s end frame. If only the end frame should be changed, see the “frame_end” property instead.\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\nframe_start¶ Type:\n\nfloat in [-inf, inf], default 0.0\n\nframe_start_raw¶\n\nSame as frame_start, except that any value can be set, including ones that create an invalid state\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\nframe_start_ui¶\n\nStart frame of the NLA strip. Note: changing this value also updates the value of the strip’s end frame. If only the start frame should be changed, see the “frame_start” property instead.\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\ninfluence¶\n\nAmount the strip contributes to the current result\n\nType:\n\nfloat in [0, 1], default 0.0\n\nlast_slot_identifier¶\n\nThe identifier of the most recently assigned action slot. The slot identifies which sub-set of the Action is considered to be for this strip, and its identifier is used to find the right slot when assigning an Action.\n\nType:\n\nstring, default “”, (never None)\n\nmodifiers¶\n\nModifiers affecting all the F-Curves in the referenced Action\n\nType:\n\n[bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of [FModifier](https://docs.blender.org/api/current/bpy.types.FModifier.html#bpy.types.FModifier), (readonly)\n\nmute¶\n\nDisable NLA Strip evaluation\n\nType:\n\nboolean, default False\n\nname¶ Type:\n\nstring, default “”, (never None)\n\nrepeat¶\n\nNumber of times to repeat the action range\n\nType:\n\nfloat in [0.1, 1000], default 1.0\n\nscale¶\n\nScaling factor for action\n\nType:\n\nfloat in [0.0001, 1000], default 1.0\n\nselect¶\n\nNLA Strip is selected\n\nType:\n\nboolean, default False\n\nstrip_time¶\n\nFrame of referenced Action to evaluate\n\nType:\n\nfloat in [-inf, inf], default 0.0\n\nstrips¶\n\nNLA Strips that this strip acts as a container for (if it is of type Meta)\n\nType:\n\n[bpy_prop_collection](https://docs.blender.org/api/current/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection) of`NlaStrip`, (readonly)\n\ntype¶\n\nType of NLA Strip\n\n`CLIP` Action Clip – NLA Strip references some Action.\n\n`TRANSITION` Transition – NLA Strip ‘transitions’ between adjacent strips.\n\n`META` Meta – NLA Strip acts as a container for adjacent strips.\n\n`SOUND` Sound Clip – NLA Strip representing a sound event for speakers.\n\nType:\n\nenum in [`'CLIP'`,`'TRANSITION'`,`'META'`,`'SOUND'`], default`'CLIP'`, (readonly)\n\nuse_animated_influence¶\n\nInfluence setting is controlled by an F-Curve rather than automatically determined\n\nType:\n\nboolean, default False\n\nuse_animated_time¶\n\nStrip time is controlled by an F-Curve rather than automatically determined\n\nType:\n\nboolean, default False\n\nuse_animated_time_cyclic¶\n\nCycle the animated time within the action start and end\n\nType:\n\nboolean, default False\n\nuse_auto_blend¶\n\nNumber of frames for Blending In/Out is automatically determined from overlapping strips\n\nType:\n\nboolean, default False\n\nuse_reverse¶\n\nNLA Strip is played back in reverse order (only when timing is automatically determined)\n\nType:\n\nboolean, default False\n\nuse_sync_length¶\n\nUpdate range of frames referenced from action after tweaking strip and its keyframes\n\nType:\n\nboolean, default False\n\nclassmethod bl_rna_get_subclass(id, default=None, /)¶ Parameters:\n\nid (str) – The RNA type identifier.\n\nReturns:\n\nThe RNA type or default when not found.\n\nReturn type:\n\n[bpy.types.Struct](https://docs.blender.org/api/current/bpy.types.Struct.html#bpy.types.Struct) subclass\n\nclassmethod bl_rna_get_subclass_py(id, default=None, /)¶ Parameters:\n\nid (str) – The RNA type identifier.\n\nReturns:\n\nThe class or default when not found.\n\nReturn type:\n\ntype\n\n## Inherited Properties¶\n\n[bpy_struct.id_data](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_data)\n\n## Inherited Functions¶\n\n[bpy_struct.as_pointer](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.as_pointer)\n\n[bpy_struct.driver_add](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.driver_add)\n\n[bpy_struct.driver_remove](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.driver_remove)\n\n[bpy_struct.get](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.get)\n\n[bpy_struct.id_properties_clear](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_clear)\n\n[bpy_struct.id_properties_ensure](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_ensure)\n\n[bpy_struct.id_properties_ui](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.id_properties_ui)\n\n[bpy_struct.is_property_hidden](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_hidden)\n\n[bpy_struct.is_property_overridable_library](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_overridable_library)\n\n[bpy_struct.is_property_readonly](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_readonly)\n\n[bpy_struct.is_property_set](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.is_property_set)\n\n[bpy_struct.items](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.items)\n\n[bpy_struct.keyframe_delete](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keyframe_delete)\n\n[bpy_struct.keyframe_insert](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keyframe_insert)\n\n[bpy_struct.keys](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.keys)\n\n[bpy_struct.path_from_id](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_from_id)\n\n[bpy_struct.path_from_module](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_from_module)\n\n[bpy_struct.path_resolve](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.path_resolve)\n\n[bpy_struct.pop](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.pop)\n\n[bpy_struct.property_overridable_library_set](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.property_overridable_library_set)\n\n[bpy_struct.property_unset](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.property_unset)\n\n[bpy_struct.rna_ancestors](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.rna_ancestors)\n\n[bpy_struct.type_recast](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.type_recast)\n\n[bpy_struct.values](https://docs.blender.org/api/current/bpy.types.bpy_struct.html#bpy.types.bpy_struct.values)\n\n## References¶\n\n[bpy.context.active_nla_strip](https://docs.blender.org/api/current/bpy.context.html#bpy.context.active_nla_strip)\n\n[bpy.context.selected_nla_strips](https://docs.blender.org/api/current/bpy.context.html#bpy.context.selected_nla_strips)\n\n`NlaStrip.strips`\n\n[NlaStrips.new](https://docs.blender.org/api/current/bpy.types.NlaStrips.html#bpy.types.NlaStrips.new)\n\n[NlaStrips.remove](https://docs.blender.org/api/current/bpy.types.NlaStrips.html#bpy.types.NlaStrips.remove)\n\n[NlaTrack.strips](https://docs.blender.org/api/current/bpy.types.NlaTrack.html#bpy.types.NlaTrack.strips)" +} +``` +User decision: **accepted** + +Status: **completed** + + +
+
+ +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "\\[[\\\"']|\\.get\\(|custom_property|animation_data\\.action", + "path": "T:\\Dynamic-Link-Manager\\ops", + "caseInsensitive": false +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "\\[[\\\"']|\\.get\\(|custom_property|animation_data\\.action", + "path": "T:\\Dynamic-Link-Manager\\ops", + "outputMode": "content", + "totalFiles": 4, + "totalMatches": 15, + "topFiles": [ + { + "uri": "ops\\migrator.py", + "matchCount": 6 + }, + { + "uri": "ops\\tweak_tools.py", + "matchCount": 1 + }, + { + "uri": "ops/library.py", + "matchCount": 2 + }, + { + "uri": "ops/migrator.py", + "matchCount": 6 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + +
Thought Process + +I now have a clear picture: + +**NLAStrip properties to copy (for timing and effects):** +- action (already passed to new()) +- action_frame_start, action_frame_end (we're not copying these - important for which part of the action is used!) +- blend_in, blend_out, blend_type, extrapolation, frame_end, repeat (we copy these) +- frame_start (we pass to new()) +- influence, mute +- scale (we're NOT copying - user said "timing" - scale affects playback speed) +- use_auto_blend, use_reverse, use_animated_influence, use_animated_time, use_animated_time_cyclic, use_sync_length + +So the plan should include copying at least: action_frame_start, action_frame_end, influence, mute, scale, use_auto_blend, use_reverse, use_animated_* flags. And we already have frame_start, frame_end, blend_*. + +**als.turn_on:** Not in this codebase; it's from an external addon (likely "Animation Layers" or Rigify-related). We read from orig (armature object or a specific bone - often these are on the root bone or the armature). I'll plan: read `orig.get("als.turn_on")` or from a pose bone that has it; set on rep after migration. Need to handle both object-level and bone-level custom props. + +**Add vs replace:** User said "MigNLA should add them" - so do not remove existing rep tracks; add orig's tracks to rep. If we add, we need to handle duplicate track names (either uniquify or append to existing track). I'll plan: add tracks (don't clear rep); optionally uniquify track names if they already exist on rep. + +**Skip NLA when only active action:** If `not orig.animation_data.nla_tracks` and `orig.animation_data.action` is set, then rep.animation_data_create() if needed, set rep.animation_data.action = orig.animation_data.action, and return. No strip copying. + +Creating the plan. +
+ +--- + + +Checking where custom properties live on armatures (object vs pose bone) and how `animation_data.action` is used: + + +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "animation_data|\\.action", + "path": "T:\\Dynamic-Link-Manager\\ops\\migrator.py", + "caseInsensitive": false +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "animation_data|\\.action", + "path": "T:\\Dynamic-Link-Manager\\ops\\migrator.py", + "outputMode": "content", + "totalFiles": 1, + "totalMatches": 14, + "topFiles": [ + { + "uri": "ops\\migrator.py", + "matchCount": 14 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py + +
+
+ +--- + +
Thought Process + +I have everything I need. One more thing: "als.turn_on" - in Blender, custom properties with a dot in the name are typically stored with the key as-is, e.g. `obj["als.turn_on"]`. So we'd do: + val = orig.get("als.turn_on") + if val is not None: + rep["als.turn_on"] = val +It could also be on the armature's data (armature.data) or on a specific bone. I'll plan to check the armature object first, then optionally the root/control bone if the addon stores it there. I'll keep the plan simple: check orig (object) for "als.turn_on", mirror to rep. + +Creating the plan. +
+ +--- + + +Creating the plan: + + +--- + + +
+Tool use: **create_plan** + + +Parameters: + +```json +{ + "plan": "# NLAMig enhancements\n\n## Current behavior\n\n- [ops/migrator.py](T:\\Dynamic-Link-Manager\\ops\\migrator.py) `run_step_2(orig, rep)` migrates NLA by:\n - Returning immediately if `orig` has no `animation_data` or no `nla_tracks`\n - Clearing all existing NLA tracks on `rep`, then copying each track and strip from `orig`\n - Copying per-strip: `blend_type`, `extrapolation`, `frame_end`, `blend_in`, `blend_out`, `repeat` (and `frame_start` via `strips.new()`)\n\n- No handling of: active action-only case, `als.turn_on`, add-vs-replace policy, or full strip timing/effects (e.g. `action_frame_start`/`action_frame_end`, `scale`, `influence`, `mute`, `use_auto_blend`, `use_reverse`).\n\n## 1. Mirror `als.turn_on` (animation layers) from base rig to RepChar\n\n- **Source:** Read from the **original** armature object after migration. The property is typically a custom property on the armature (e.g. `orig[\"als.turn_on\"]`) from an external addon (e.g. Rigify Animation Layers). If the addon stores it on a specific bone, check that bone on `orig` and mirror to the same bone on `rep`.\n- **Implementation:** After migrating tracks/strips (or after applying active action in the no-NLA case), if `\"als.turn_on\"` exists on `orig` (e.g. `orig.get(\"als.turn_on\")` or on a known control bone), set the same value on `rep` in the same location (object or same-named bone). Use a small helper so the same logic runs for both the “NLA migration” path and the “active action only” path.\n- **Scope:** Only mirror; do not add or remove animation layer UI. The addon that owns `als.turn_on` will interpret the value.\n\n## 2. MigNLA adds layers (do not replace; no user prompt)\n\n- **Current:** All `rep.animation_data.nla_tracks` are removed, then tracks from `orig` are created.\n- **Change:** Do **not** remove existing tracks on `rep`. For each track on `orig`, **add** a new track on `rep` (e.g. `rep.animation_data.nla_tracks.new(prev=prev_track)` after the last existing or newly added track). If track names must be unique and `rep` already has a track with the same name, either uniquify the new track name (e.g. append a suffix) or append strips to the existing track—prefer uniquify to avoid mixing different layer semantics. Result: MigNLA always “adds” the migrated layers; no dialog for “remove or add” (that stays in the Animlayers addon only).\n\n## 3. Skip NLA when only an active action exists\n\n- **Condition:** `orig.animation_data` exists, `orig.animation_data.nla_tracks` is empty (or absent), and `orig.animation_data.action` is set.\n- **Behavior:** Do not run full NLA track/strip migration. Ensure `rep` has `animation_data` (create if missing), set `rep.animation_data.action = orig.animation_data.action`, then run the same `als.turn_on` mirror logic and return.\n- **Placement:** At the start of `run_step_2`, after the “no animation_data” early return: if there is animation_data but no nla_tracks and an active action, do the active-action + als.turn_on path and return.\n\n## 4. Preserve strip timing and effects on RepChar\n\n- **Goal:** Strips that are offset or scaled on the timeline (not “full frame range like normal AnimLayers”) must keep the same timing and other strip effects on `rep`.\n- **Already copied:** `frame_start` (via `strips.new()`), `frame_end`, `blend_in`, `blend_out`, `blend_type`, `extrapolation`, `repeat`.\n- **Add copying** for all relevant writable NLA strip properties so timing and effects are mirrored (see [NlaStrip](https://docs.blender.org/api/current/bpy.types.NlaStrip.html)):\n - **Timing/range:** `action_frame_start`, `action_frame_end` (which part of the action is used)\n - **Influence and playback:** `influence`, `mute`, `scale`, `use_reverse`\n - **Flags:** `use_auto_blend`, `use_animated_influence`, `use_animated_time`, `use_animated_time_cyclic`, `use_sync_length`\n- **Read-only / complex:** Do not copy `fcurves` (strip influence/time F-curves) in a first pass unless needed; if the addon relies on them, a follow-up can duplicate fcurves. Document that strip-level animation (e.g. animated influence) may require extra work if needed later.\n\n## 5. Order of operations in `run_step_2`\n\n1. If no `orig.animation_data`: return.\n2. **Active-action-only path:** If `orig.animation_data.nla_tracks` is empty and `orig.animation_data.action` is set: ensure `rep.animation_data`, set `rep.animation_data.action`, mirror `als.turn_on`, return.\n3. **NLA path:** Ensure `rep.animation_data`. Do **not** remove existing tracks on `rep`.\n4. For each track in `orig.animation_data.nla_tracks`: add a new track on `rep` (with uniquified name if needed), copy track props, then for each strip create new strip and copy all timing/effect properties listed above (including `action_frame_start`/`action_frame_end`, `scale`, `influence`, `mute`, and the use_* flags).\n5. Mirror `als.turn_on` from `orig` to `rep` (same as in step 2).\n\n## Files to change\n\n- **[ops/migrator.py](T:\\Dynamic-Link-Manager\\ops\\migrator.py):** Refactor `run_step_2` to implement the logic above (active-action shortcut, add-only tracks, full strip property copy, and a shared `_mirror_als_turn_on(orig, rep)` helper). No API changes to `run_step_2(orig, rep)` signature.\n\n## Out of scope\n\n- No changes to the Animlayers addon (its remove/add prompt stays as-is).\n- No UI changes; operator `dlm.migrator_migrate_nla` continues to call `run_step_2` only.\n", + "name": "NLAMig enhancements", + "overview": "Extend the NLA migration step to: (1) mirror the base rig's als.turn_on (animation layers) setting to RepChar after migration; (2) add migrated layers to RepChar instead of replacing (no user prompt); (3) when OrigChar has no NLA tracks and only an active action, skip NLA and apply that action to RepChar; (4) preserve full strip timing and effects (frame range, scale, influence, action_frame_start/end, etc.) on migrated strips." +} +``` +Additional data: + +```json +{ + "reviewData": { + "status": "Requested", + "selectedOption": "none", + "isShowingInput": false + }, + "planUri": "file:///c%3A/Users/Nathan/.cursor/plans/nlamig_enhancements_8f0d5626.plan.md", + "hasOpenedEditor": true +} +``` +Result: + +```json +{ + "rejected": {} +} +``` +Status: **completed** + + +
+
+ +--- + +_**User**_ + +Implement the plan as specified, it is attached for your reference. Do NOT edit the plan file itself. + +To-do's from the plan have already been created. Do not create them again. Mark them as in_progress as you work, starting with the first one. Don't stop until you have completed all the to-dos. + +--- + +_**Agent (model default, mode Agent)**_ + +
Thought Process + +The user wants me to implement the NLAMig enhancements plan. Let me read the plan again and the current migrator code, then implement: + +1. Mirror `als.turn_on` from base rig to RepChar (helper + call after both paths) +2. MigNLA adds layers (don't remove existing tracks; uniquify names if needed) +3. Skip NLA when only active action exists (copy action, mirror als.turn_on, return) +4. Preserve full strip timing/effects (action_frame_start/end, influence, mute, scale, use_* flags) +5. Order: no anim_data return → active-action-only path → NLA path (add tracks) → mirror als.turn_on + +I need to check if there are todos in the plan - the user said "To-do's from
+ +--- + diff --git a/ops/migrator.py b/ops/migrator.py index 03824ee..f20bae0 100644 --- a/ops/migrator.py +++ b/ops/migrator.py @@ -47,30 +47,81 @@ def run_step_1(orig, rep): rep.scale = orig.scale.copy() +def _mirror_als_turn_on(orig, rep): + """Mirror als.turn_on (animation layers) from original to replacement armature.""" + key = "als.turn_on" + if key in orig: + try: + rep[key] = orig[key] + except Exception: + pass + for pbone in orig.pose.bones: + if pbone.name not in rep.pose.bones or key not in pbone: + continue + try: + rep.pose.bones[pbone.name][key] = pbone[key] + except Exception: + pass + + def run_step_2(orig, rep): - """Migrate NLA: copy tracks and strips to replacement.""" - if not orig.animation_data or not orig.animation_data.nla_tracks: + """Migrate NLA: copy tracks and strips to replacement; or apply active action only.""" + if not orig.animation_data: + return + ad = orig.animation_data + has_nla = ad.nla_tracks and len(ad.nla_tracks) > 0 + active_action = getattr(ad, "action", None) + if not has_nla and active_action: + if rep.animation_data is None: + rep.animation_data_create() + rep.animation_data.action = active_action + _mirror_als_turn_on(orig, rep) + return + if not has_nla: return if rep.animation_data is None: rep.animation_data_create() - for track in list(rep.animation_data.nla_tracks): - rep.animation_data.nla_tracks.remove(track) - prev_track = None - for track in orig.animation_data.nla_tracks: - new_track = rep.animation_data.nla_tracks.new(prev=prev_track) - new_track.name = track.name + rep_tracks = rep.animation_data.nla_tracks + existing_names = {t.name for t in rep_tracks} + prev_track = rep_tracks[-1] if rep_tracks else None + for track in ad.nla_tracks: + new_track = rep_tracks.new(prev=prev_track) + name = track.name + if name in existing_names: + base, n = name, 1 + while f"{base}.{n:03d}" in existing_names: + n += 1 + name = f"{base}.{n:03d}" + new_track.name = name + existing_names.add(name) new_track.mute = track.mute new_track.is_solo = track.is_solo new_track.lock = track.lock for strip in track.strips: - new_strip = new_track.strips.new(strip.name, int(strip.frame_start), strip.action) + if strip.type != "CLIP" or not strip.action: + continue + new_strip = new_track.strips.new( + strip.name, int(strip.frame_start), strip.action + ) new_strip.blend_type = strip.blend_type new_strip.extrapolation = strip.extrapolation new_strip.frame_end = strip.frame_end new_strip.blend_in = strip.blend_in new_strip.blend_out = strip.blend_out new_strip.repeat = strip.repeat + new_strip.action_frame_start = strip.action_frame_start + new_strip.action_frame_end = strip.action_frame_end + new_strip.influence = strip.influence + new_strip.mute = strip.mute + new_strip.scale = strip.scale + new_strip.use_auto_blend = strip.use_auto_blend + new_strip.use_reverse = strip.use_reverse + new_strip.use_animated_influence = strip.use_animated_influence + new_strip.use_animated_time = strip.use_animated_time + new_strip.use_animated_time_cyclic = strip.use_animated_time_cyclic + new_strip.use_sync_length = strip.use_sync_length prev_track = new_track + _mirror_als_turn_on(orig, rep) EXCLUDE_PROPS = {"_RNA_UI", "rigify_type", "rigify_parameters"}