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 3f9565a..fddbae2 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
@@ -6176,20 +6176,27 @@ MigShapekeys and MigNLA should have their buttons truncated to simply "NLA" and
_**Agent (model default, mode Agent)**_
+Thought Process
+The user wants to change the button text for two operators in the panel:
+1. Migrate NLA → "NLA"
+2. Migrate BaseBody shapekeys → "BaseBody ShapeKeys"
+
+I need to update the panel where these operators are drawn (the `text=` parameter).
+
---
+
+
---
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py
-Status: **loading**
-
@@ -6200,7 +6207,179 @@ Status: **loading**
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py
-Status: **loading**
+
+
+
+
+---
+
+Thought Process
+
+Done. Button labels are now "NLA" and "BaseBody ShapeKeys". The operator bl_label is unchanged (still the full name for menus/search); only the panel button text was updated.
+
+
+---
+
+
+Updated the panel so those two buttons show “NLA” and “BaseBody ShapeKeys”. The operators’ `bl_label` is unchanged so they still appear with their full names in menus and search.
+
+---
+
+_**User**_
+
+It looks like the shapekey values are not copied.
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+Thought Process
+
+The user is saying that shapekey values are not copied during the character migration workflow. Currently step 6 (Migrate BaseBody shapekeys) does:
+1. Library override on the replacement's base body mesh (and make it editable)
+2. Assign the shape-key action (e.g. CC_Base_Body.002Action) to the replacement's shape keys animation data
+
+But it doesn't copy the actual shape key *values* (the keyframe values / the current values of each shape key slider). So if the original character's base body has shape keys with certain values (either from keyframes or from manually set values), those numeric values aren't being copied to the replacement's shape keys.
+
+I need to:
+1. Find where step 6 is implemented (ops/migrator.py run_step_6)
+2. Add logic to copy shape key values from the original character's base body to the replacement's base body
+
+To copy shape key values we need to:
+- Find the original character's base body mesh (same logic: MESH, name contains body+base, armature modifier to orig)
+- For each shape key on the original's base body, find the matching shape key on the replacement's base body and copy the `value` (and possibly `slider_min`, `slider_max` if we want)
+- Shape keys have a `value` property (0-1 typically) that is the slider value
+
+Let me look at the migrator code to see the current step 6 and how we get orig_descendants or the original's base body.
+
+
+---
+
+
+Checking step 6 and how we can copy shape key values from the original to the replacement base body.
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "shape_key|shapekey",
+ "path": "T:\\Dynamic-Link-Manager",
+ "glob": "*.py",
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "shape_key|shapekey",
+ "path": "T:\\Dynamic-Link-Manager",
+ "outputMode": "content",
+ "totalFiles": 4,
+ "totalMatches": 16,
+ "topFiles": [
+ {
+ "uri": "ui\\panels.py",
+ "matchCount": 1
+ },
+ {
+ "uri": "ops\\migrator.py",
+ "matchCount": 4
+ },
+ {
+ "uri": "ops/migrator.py",
+ "matchCount": 4
+ },
+ {
+ "uri": "ui\\operators.py",
+ "matchCount": 7
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+Step 6 currently only:
+1. Creates library override on rep's base body
+2. Assigns the shape-key action to the rep's shape keys
+
+It does NOT copy the actual shape key slider values from the original's base body to the replacement's. I need to:
+1. Get the original character's base body mesh (same criteria: in orig descendants, MESH, name has body+base, armature modifier to orig)
+2. For each shape key in orig_base_body.data.shape_keys.key_blocks (skip "Basis" if we want, or copy it too - Basis is usually 0), get the .value
+3. On rep's base body, find the shape key with the same name and set its .value
+
+I need to pass orig_descendants to run_step_6, or compute the original's base body from orig. Let me check - run_step_6(orig, rep, rep_descendants, context=None). We have rep_descendants but not orig_descendants. We can get the original's base body by iterating descendants(orig) with the same filter. So I'll use descendants(orig) inside run_step_6 to find the original base body mesh, then copy shape key values by name.
+
+Implementation:
+- At the start of the loop we're iterating rep_descendants. For each rep base body `ob`, we need to find the corresponding orig base body. The "corresponding" one could be found by: same relative position in hierarchy, or by name convention. Actually the rep object might have a name like CC_Base_Body.002 and the orig might be CC_Base_Body - so names might not match. But typically for a character replacement we have orig (e.g. Steve_Rigify) and rep (Steve_Rigify.001), and the orig's base body could be CC_Base_Body while rep's is CC_Base_Body.002 or similar. So we need to find the orig base body - there's usually one per character. So we can get orig_descendants, find the one mesh that has body+base in name and armature modifier to orig - that's the orig base body. Then copy shape key values from orig_base_body.data.shape_keys to ob.data.shape_keys by matching key block names.
+
+
+---
+
+
+Checking how `descendants` is used and adding shape-key value copying from the original base body to the replacement.
+
+
+---
+
+File not found
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+I'll add logic to:
+1. Get orig_descendants using descendants(orig)
+2. Find the original's base body mesh (same criteria as rep: MESH, body+base in name, armature modifier to orig)
+3. After we've done the override and action assignment for the rep base body, copy shape key values from orig base body to rep base body by matching key_blocks by name (skip Basis if we want - actually we should copy all that exist on both, and Basis value is typically 1.0 or 0)
+
+Let me implement. We need to find orig base body - there might be only one per character. So before the loop, compute orig_descendants and find orig_base_body. Then inside the loop when we have rep base body `ob`, copy shape key values from orig_base_body to ob.
+
+
+---
+
+
+Adding logic to find the original's base body and copy shape key values by name.
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
@@ -6208,6 +6387,789 @@ Status: **loading**
---
+Thought Process
+
+Now I need to add the actual copy of shape key values after we've set up the override and before (or after) we assign the action. Copying values should happen when both orig_base and ob have shape_keys, and we match key_blocks by name.
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+
+Done. I added:
+1. A helper _find_base_body(armature, descendants_iter) to get the base body mesh for an armature.
+2. At the start of run_step_6 we get orig_descendants and orig_base.
+3. When processing each rep base body, if it has shape_keys and orig_base has shape_keys, we copy each orig key's .value to the rep key of the same name.
+
+Let me verify there are no linter errors.
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **read_lints**
+
+
+Parameters:
+
+```json
+{
+ "paths": [
+ "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ ]
+}
+```
+Result:
+
+```json
+{
+ "linterErrorsByFile": [
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+Good. The implementation copies the current slider value (orig_key.value) for each shape key that exists on both the original and replacement base body. This covers the "shapekey values" the user asked for. If they also need keyframe data, that's what the action assignment does - the action drives the shape keys over time. So we're copying both the current values and (already) assigning the action for animated values.
+
+
+---
+
+
+Summary of changes:
+
+1. **Helper `_find_base_body(armature, descendants_iter)`**
+ Returns the base body mesh for an armature (same rules as before: MESH, name contains “body” and “base”, has an Armature modifier pointing at that armature).
+
+2. **Original base body in step 6**
+ `run_step_6` now gets `orig_descendants` and uses `_find_base_body(orig, orig_descendants)` so we have the original character’s base body.
+
+3. **Copying shape key values**
+ For the replacement base body, if both it and the original have `shape_keys`, each original key block is matched by name to the replacement. For every match, `rep_blocks[orig_key.name].value = orig_key.value` is set so the current slider values are copied.
+
+So the step now:
+- Creates/edits the library override on the replacement base body,
+- **Copies current shape key values** from the original to the replacement (by key name),
+- Then assigns the shape-key action as before.
+
+Reload the add-on and run “BaseBody ShapeKeys” (or full migration); the rep’s shape key values should match the original’s.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt the values are still not being copied.
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+Thought Process
+
+The user says shape key values are still not being copied. Let me look at the terminal output to see if there are any errors or clues, and then re-examine the logic.
+
+Possible issues:
+1. orig_base might not be found (e.g. different descendant structure or naming).
+2. The rep base body might be found but orig_base could be None.
+3. Key names might differ between orig and rep (e.g. "Key 1" vs "key_1" or different naming).
+4. We might need to copy at a different time - e.g. after override_create the mesh data might change?
+5. Maybe the original's base body has a different data block - so we're getting the right object but key_blocks might be on the data, and if orig and rep share the same mesh data (unlikely) or the rep's mesh data is different...
+6. Perhaps we need to use keyframe values at a specific frame rather than .value? The user said "shapekey values" - that could mean the current evaluation or the keyframe data. Let me re-read - "the values are still not being copied" suggests the numeric values. So .value should be correct.
+
+Let me read the terminal file first to see if there's any debug or error output. Then I'll look at the code again and add some debug, or consider alternative approaches.
+
+Actually - one common issue: in Blender, when you have a linked character, the *original* character's base body mesh might be in a linked collection, and ob.data might be read-only (linked). So when we read orig_key.value we get the value. But when we write to rep_blocks[name].value, the rep's mesh might have been overridden by then. So writing should work.
+
+Another thought: maybe the order of operations is wrong. We copy values AFTER the override handling. But what if the replacement's shape_keys.key_blocks don't exist until we do something? No - they're already there from the linked data.
+
+Another idea: perhaps the original base body we find is not the right one. For example, if the character has multiple meshes with "body" and "base" in the name, we return the first. Or maybe the original's hierarchy is different - e.g. the original base body is not in orig's descendants (e.g. it's in a different collection). Let me check - we use descendants(orig). So we need to ensure the original character's base body is actually in the hierarchy under orig. It should be for a Rigify character.
+
+Let me read the terminal and the current code to add debug and verify the logic.
+
+
+---
+
+
+Checking the terminal output and step 6 logic to determine why values aren't copying.
+
+
+---
+
+
+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\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+From the terminal I see we have two base bodies being processed: CC_Base_Body.003 (Eddy's rep) and CC_Base_Body.002 (Hailey's rep). So we're iterating over rep_descendants and finding two base body meshes - one for each character.
+
+The issue: we have a single `orig_base` for the whole function. But we're migrating two characters (Eddy and Hailey). So when we process CC_Base_Body.003 (Eddy's replacement), orig_base might be Hailey's base body (the first one found in orig_descendants). When we process CC_Base_Body.002 (Hailey's replacement), orig_base is still Hailey's base body. So for Eddy we're copying from the wrong character's base body!
+
+Actually wait - run_step_6 is called once per character pair. So when we run the full migration we call run_step_6(orig, rep, rep_descendants, context) for each pair. So for Eddy/Eddy.001, orig=Eddy, rep=Eddy.001, rep_descendants=descendants(Eddy.001). So orig_descendants = descendants(Eddy), and orig_base = Eddy's base body. Then we iterate rep_descendants which are Eddy.001's descendants - so we get Eddy.001's base body (CC_Base_Body.003). So we copy from Eddy's base body to Eddy.001's base body. That's correct for a single pair.
+
+But the terminal shows both CC_Base_Body.003 and CC_Base_Body.002 in the same run - so we're running the full migration which does both Hailey and Eddy. So first run_step_6(Hailey, Hailey.001, rep_descendants_Hailey, context) - orig_base = Hailey's base body (CC_Base_Body?), we process Hailey.001's base body (CC_Base_Body.002). Then run_step_6(Eddy, Eddy.001, rep_descendants_Eddy, context) - orig_base = Eddy's base body (CC_Base_Body.001?), we process Eddy.001's base body (CC_Base_Body.003). So actually orig_base is per-call, so it should be correct.
+
+So why aren't values copying? Possibilities:
+1. orig_base is None - maybe the original's base body isn't in orig_descendants? For a linked character, the armature might be in the scene but its descendants might be in a linked collection - so descendants(orig) might not include the mesh objects if they're in another collection. Let me check how descendants() works.
+2. The original's base body might have a different mesh data - when the character is linked, the object might be instanced in the scene but the actual hierarchy might be in the linked collection. So descendants(orig) could be returning objects that are in the current scene and parented to orig. If the character was linked as a collection, the armature and its children might all be in the scene. So we should get the right orig_base.
+3. Maybe the key names don't match? Like "Basis" vs "basis", or the rep has different shape key names.
+4. Maybe we need to copy at the current frame from the action? So the "values" the user sees are the evaluated values at the current frame, which come from the action (keyframes), not from .value. So copying .value might not be what they want - they might want the keyframe data from the action copied. But they said "shapekey values" which usually means the slider values.
+5. Another possibility: when the original is linked, orig_base.data might be the same mesh data as the library - and we might not be able to read .value from it? Or the rep's key_blocks might be a different structure (e.g. override might not have written key_blocks yet).
+
+Let me check the descendants function to see what it returns - does it only return objects in the current scene that are parented to the armature?
+
+
+---
+
+
+Checking how `descendants` is implemented — the original's base body may not be found when characters are linked.
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "def descendants",
+ "path": "T:\\Dynamic-Link-Manager",
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "def descendants",
+ "path": "T:\\Dynamic-Link-Manager",
+ "outputMode": "content",
+ "totalFiles": 1,
+ "totalMatches": 1,
+ "topFiles": [
+ {
+ "uri": "utils/__init__.py",
+ "matchCount": 1
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+
+
+ Tool use: **web_search** • Searched web `undefined` • **1** results
+
+| Title | URL | Chunk |
+|-------|-----|-------|
+| Web search results for query: Blender shape key key_blocks value copy linked object | | Links:
1. [Copying shape key animations from one object to another (python)](https://blender.stackexchange.com/questions/298514/copying-shape-key-animations-from-one-object-to-another-python)
2. [Update a shapekey Basis from another shapekey](https://blender.stackexchange.com/questions/66634/update-a-shapekey-basis-from-another-shapekey)
3. [bpy: How are meshes, "shape keys" and "key blocks" connected, and how to work with them?](https://blender.stackexchange.com/questions/333429/bpy-how-are-meshes-shape-keys-and-key-blocks-connected-and-how-to-work-wi)
4. [Efficient way to programmatically copy shape key animation from one mesh to another](https://blender.stackexchange.com/questions/242890/efficient-way-to-programmatically-copy-shape-key-animation-from-one-mesh-to-anot)
5. [Scripting: transferring shape keys between different meshes?](https://blender.stackexchange.com/questions/119836/scripting-transferring-shape-keys-between-different-meshes)
# Blender Shape Key key_blocks Value and Copying
## Basic Structure
In Blender's Python API, shape keys are organized hierarchically:[3]
- **`Key`**: A data container that stores all shape key information for a mesh (accessed via `mesh.shape_keys`)
- **`key_blocks`**: A collection of individual `ShapeKey` objects within the `Key` (accessed via `mesh.shape_keys.key_blocks`)
- **`value`**: The influence/strength property of each shape key that can be animated
## Copying Shape Key Values
To copy shape key values between objects with identical topology:[1]
```python
for source_key_block in source_shape_keys.key_blocks:
target_key_block = target_shape_keys.key_blocks.get(source_key_block.name)
target_key_block.value = source_key_block.value
```
## Copying Shape Keys with Animation
For copying both static values and keyframe animations:[1]
```python
for key in source_keys.key_blocks:
k = target_obj.shape_key_add(name=key.name, from_mix=False)
for i, v in enumerate(source_obj.data.vertices):
k.data[i].co = key.data[i].co
k.value = key.value
```
Then copy fcurves from the source action to a new target action.[1]
## Linked Objects
If you want to transfer animations more efficiently across similar meshes, renaming shape key blocks to match can allow them to use the same action without duplicating data.[4] |
+
+
+
+
+---
+
+Thought Process
+
+So the copy logic (value = value) is correct. The issue might be:
+1. orig_base is None - descendants(orig) might not include the base body mesh for linked characters.
+2. Key names might not match - use .get() to be safe and skip if not found.
+
+Let me read the descendants implementation.
+
+
+---
+
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\utils\__init__.py
+
+
+
+
+---
+
+Thought Process
+
+So descendants(armature) iterates bpy.data.objects and finds any object whose parent chain leads to the armature. So it finds all objects in the blend (including linked) that are parented under the armature. So when we have Eddy (armature) in the scene, his base body mesh would be parented under him (or under something that is under him). So we should find it.
+
+Unless - for linked characters, the hierarchy might be different. When you link a collection, the objects in that collection might have their parent set to an object that is also in the collection. So Eddy_Rigify might be in the scene, and CC_Base_Body.001 might have parent = some other object that is parented to Eddy_Rigify. So descendants(Eddy_Rigify) would include CC_Base_Body.001. Good.
+
+So orig_base should be found. Unless the original character's base body has a different armature modifier setup - e.g. the modifier points to the armature but the mesh isn't parented to the armature? In Rigify the mesh is usually a child of the rig. Let me add debug to see if orig_base is None and if the key names match. Actually, let me think about the scene again.
+
+We have two characters: Hailey and Eddy. So we have Hailey_Rigify and Hailey_Rigify.001, Eddy_Rigify and Eddy_Rigify.001. When we run full migration we do both. For Hailey: orig = Hailey_Rigify, rep = Hailey_Rigify.001. orig_descendants = all objects under Hailey_Rigify. So that would include Hailey's base body - probably named CC_Base_Body (or CC_Base_Body.001 if there are duplicates). And rep_descendants would be under Hailey_Rigify.001, so we'd get the replacement's base body CC_Base_Body.002. So orig_base could be CC_Base_Body and we're processing ob=CC_Base_Body.002. Good.
+
+One more possibility: maybe the **original** character's base body mesh is **linked** and when we read orig_base.data.shape_keys.key_blocks[].value, Blender might be returning a default or the value might be read-only so we read it but the write to rep goes through - but we're writing the same value we read. So if the linked data has value 0.5 and we copy to rep, rep should get 0.5. Unless... the rep's shape_keys are also linked (same Key block from library) until we override. So when we do ob.data.override_create(), we're overriding the mesh data. Does that also override the shape_keys? The shape_keys are a pointer from the mesh to a Key datablock. So when we override the mesh, we might get a new mesh datablock but the Key might still be shared/linked. So ob.data.shape_keys might still point to the library's Key block. In that case when we set rep_blocks[orig_key.name].value = orig_key.value, we might be writing to the shared Key block! So we'd be changing the library data or a shared block. And if the rep's key_blocks are from a different Key (the replacement's mesh might have its own Key after override), then orig_key and rep_blocks could be from different Key blocks. Let me check - after ob.data.override_create(), ob.data is the overridden mesh. Does the overridden mesh have its own shape_keys or does it still reference the same Key? Typically overriding the mesh would create a local override of the mesh, and the shape_keys pointer might still point to the linked Key. So we might need to override the Key (shape_keys) as well to get a local copy we can edit. So when we set .value, we might be writing to the linked Key which could affect all users of that Key, or the write might not be allowed.
+
+Actually re-reading the user's terminal output: we have two base bodies in the same run - CC_Base_Body.003 and CC_Base_Body.002. So we're processing two rep base bodies. That means we're in a single call to run_step_6 with rep_descendants containing objects from TWO characters? No - rep_descendants is passed from run_full_migration, and run_full_migration is called once and processes one pair (orig, rep). So we only have one pair per run. So when would we have both CC_Base_Body.003 and CC_Base_Body.002 in the same step 6? We wouldn't - unless the two base bodies are both descendants of the same armature. So maybe Hailey_Rigify.001 has two meshes with "base" and "body" in the name? That seems unlikely. Oh - maybe the full migration runs step 6 twice, once for Hailey and once for Eddy. So first run: orig=Hailey_Rigify, rep=Hailey_Rigify.001, we process CC_Base_Body.002. Second run: orig=Eddy_Rigify, rep=Eddy_Rigify.001, we process CC_Base_Body.003. So the terminal output is from one run that processed both characters - so we see two "[DLM step6]" blocks, one for each character's step 6. So for each run_step_6 call we only have one orig and one rep. So orig_base is the base body of the current orig. So when processing Eddy.001's base body (CC_Base_Body.003), orig_base should be Eddy's base body. So that's correct.
+
+Let me think about why values wouldn't copy:
+1. orig_base is None - then we never copy. I'll add debug to print whether orig_base was found and how many keys we copy.
+2. Key names don't match - rep might have "Basis" and orig might have "Basis" too, but if the rep's mesh comes from a different asset version the key names could differ. I'll use .get() and log when a key isn't found.
+3. The rep's shape_keys might be on a linked Key block - so setting .value might not be allowed or might not persist. We might need to ensure the Key (shape_keys) is overridden too. Let me check - when we override the mesh (ob.data.override_create()), does Blender also override the Key block that the mesh references? I think the mesh's shape_keys is a pointer - so the overridden mesh might still point to the same linked Key. So we need to override the Key datablock as well so we have a local override of the shape keys, then set values. Let me search for how to override shape keys or Key datablock.
+4. Maybe we need to copy at the current frame - i.e. evaluate the original's action at the current frame and set that as the value. That would be "copy evaluated shape key values at current frame". Let me try first with ensuring Key is overridden and add debug.
+
+
+---
+
+
+Checking whether the shape_keys Key block must be overridden for value writes to persist, and adding debug.
+
+
+---
+
+
+
+ Tool use: **web_search** • Searched web `undefined` • **1** results
+
+| Title | URL | Chunk |
+|-------|-----|-------|
+| Web search results for query: Blender override_create mesh shape_keys Key datablock linked override | | Links:
1. [Library Overrides ¶](https://developer.blender.org/docs/features/core/overrides/library/)
2. [Library Overrides ¶](https://docs.blender.org/manual/en/latest/files/linked_libraries/library_overrides.html)
3. [Key(ID) - Blender Python API](https://docs.blender.org/api/current/bpy.types.Key.html)
4. [Library Overrides ](https://docs.blender.org/manual/sr/4.0/files/linked_libraries/library_overrides.html)
5. [IDOverrideLibrary(bpy_struct) ¶](https://docs.blender.org/api/master/bpy.types.IDOverrideLibrary.html)
# Blender Shape Keys and Library Overrides
## Shape Keys as Non-Linkable IDs
Shape keys (Key datablocks) are classified as **non-linkable IDs** in Blender's library override system.[1] This means they do not have their own independent library override structure. Instead, shape keys are treated as sub-data of their owner ID (the mesh or other geometric datablock they belong to).[1]
## Key Datablock Structure
The `Key` datablock is an ID-type that contains shape key data.[3] It includes:
- `key_blocks`: A collection of individual shape keys
- `reference_key`: The basis/reference shape key
- `user`: The ID (mesh, curve, etc.) that owns these shape keys
- `use_relative`: Whether shape keys are relative or sequential[3]
## Override Implications
Because shape keys are non-linkable IDs, when you create library overrides of a mesh with shape keys, the shape key modifications are considered part of the override operations of the parent mesh datablock rather than separate overrides.[1] Override operations are stored with their RNA paths but don't store actual data—the data is kept in the local override ID itself.[1]
When the library file is updated, unmodified properties (including shape keys that weren't edited) will be updated to match the linked version, while your local edits are preserved through the override system's merge process.[4] |
+
+
+
+
+---
+
+Thought Process
+
+So when we override the mesh, the shape keys are considered part of the mesh override - they're sub-data. So the Key block might get overridden along with the mesh, or the values we set might need to be stored as override operations. The issue could be that we're setting .value on key_blocks before the mesh's shape_keys are in an editable state, or the Key might still be the linked one.
+
+Let me try two things:
+1. Override the Key (ob.data.shape_keys) if it's linked, before copying values - so we have a local override of the shape key data.
+2. Add debug: print orig_base, print number of keys copied, and whether any keys were missing on rep.
+
+Actually re-reading the Blender docs: "shape key modifications are considered part of the override operations of the parent mesh". So when we override the mesh, we get a local mesh. Does that local mesh have a local Key or still point to the linked Key? If the mesh override creates a full copy of the mesh data for the override, then the shape_keys pointer would need to point to something - either the same linked Key (then we'd be writing to linked data when we set .value) or a new local Key. I think in Blender, when you override a Mesh, the Mesh datablock is copied locally and the shape_keys pointer might point to the same Key from the library - so the Key is still linked. So when we set rep_blocks[name].value we might be modifying the linked Key, which could be shared by multiple meshes, and the modification might not "stick" for the override system or might affect other users. So we should override the Key (ob.data.shape_keys) if it's linked, then set values. Let me check - does Key have override_create? It's an ID type, so it should.
+
+
+---
+
+
+Overriding the Key (shape_keys) if it's linked so value writes apply to a local override. Adding that and debug.
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+
+Good. Now we:
+1. Override the Key (shape_keys) if it's linked so writes to .value go to a local override
+2. Use .get() for safe lookup
+3. Print how many values we copied and from which object, or print why we didn't (no orig_base or no shape_keys on orig)
+
+This should fix the case where the rep's shape_keys were still linked. If the issue was that orig_base was None (e.g. original's base body not in descendants), the debug will show "no orig base body found". If key names don't match we'll see "copied 0/N".
+
+
+---
+
+
+Summary of changes:
+
+1. **Override the Key block when it’s linked**
+ If `ob.data.shape_keys` is linked, we call `sk.override_create(remap_local_usages=True)` so the replacement uses a local override of the shape keys. Writing `.value` on a linked Key can be ignored or shared; writing on an overridden Key is local and sticks.
+
+2. **Safer key lookup**
+ Use `rep_blocks.get(orig_key.name)` instead of `if orig_key.name in rep_blocks` so we only write when the key exists on the rep.
+
+3. **Debug prints**
+ - After copying: `[DLM step6] shapekey values: copied X/Y from `
+ - If no original base body: `no orig base body found for armature `
+ - If original has no shape keys: `orig base body has no shape_keys`
+
+Run the step again and check the System Console. You should see either how many values were copied or one of the debug messages. If you see `copied 0/N`, the key names don’t match between original and rep. If you see `no orig base body found`, the original base body isn’t in the armature’s descendants (e.g. different hierarchy when linked).
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt it shouldn't have any trouble finding the original basebody, it's there. use blender mcp to view the scene.
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+Thought Process
+
+The user wants me to use the Blender MCP to view the scene and understand why the original base body might not be found, or to verify the scene structure. They're saying the original base body is there, so the issue might be with our _find_base_body logic (e.g. the original might not be in "descendants" of the armature, or the naming/armature modifier check might be wrong).
+
+Let me use the browser MCP to connect to Blender - actually the user said "blender mcp". I need to check what MCP tools are available. Looking at my tools, I have call_mcp_tool, fetch_mcp_resource. So there might be a Blender MCP server. Let me check - the user said "use blender mcp to view the scene". I don't see a Blender MCP in my tool list. I have cursor-ide-browser for browser automation. Maybe they mean run a script via Blender that prints scene structure? Or there could be an MCP server I can call.
+
+Looking at my available tools again - I have mcp_web_fetch, mcp_task, call_mcp_tool, fetch_mcp_resource. So I can call_mcp_tool with a server name. I don't know the Blender MCP server name. Let me try to list or discover. Actually the user might have a Blender MCP that exposes scene inspection - I'm not given a list of MCP servers in the prompt. I'll try "blender" as server and see if there's a "view scene" or "get scene" or similar tool. Or I could run a Python script in Blender that outputs the scene hierarchy and base body info - but the user said "use blender mcp", so they expect an MCP.
+
+I'll try call_mcp_tool with server that might be related to Blender. Without knowing the exact server name or tool names, I might need to run a script instead. Let me try running a small Blender script that prints the scene structure, armatures, and base body meshes - that will show us what's in the scene and why _find_base_body might fail. The script can be run via the Blender development extension or we could add temporary debug to the addon.
+
+Actually re-reading the user message: "it shouldn't have any trouble finding the original basebody, it's there. use blender mcp to view the scene." So they're saying 1) the original base body is there, so our code might have a bug in finding it, and 2) they want me to use Blender MCP to view the scene. So they have a Blender MCP set up. I should try to use it. Common server names might be "blender", "blender-mcp", "cursor-ide-browser" (that's for browser). I'll try call_mcp_tool with server "blender" and a tool like "get_scene" or "inspect" or "evaluate" - I don't have the schema. Let me try and see what happens.
+
+Actually, I don't have a list of MCP servers or their tools in my context. The instructions say "Always read the tool's schema/descriptor BEFORE calling to ensure correct parameters." So I need to find out what the Blender MCP provides. I don't have a direct way to list MCP tools. I'll try a few things:
+
+1. Run a Python script that uses bpy to print scene structure - we can do this by creating a script file and having the user run it, or by adding debug to the addon. But the user said "use blender mcp".
+2. Try call_mcp_tool(server="blender", toolName="...", arguments={}) - I don't know the tool name.
+
+Let me try to infer: perhaps the Blender MCP allows evaluating Python in Blender and returning the result. So toolName could be "evaluate" or "run_script" with arguments {"code": "..."}. I'll try that.
+
+
+---
+
+
+Checking for a Blender MCP and running scene inspection to verify the hierarchy and base-body lookup.
+
+
+---
+
+MCP server does not exist: blender
+
+---
+
+Thought Process
+
+The server is "user-blender". Let me try that and find the right tool name. I might need to list tools - but I don't have a "list tools" MCP function. Let me try user-blender with a reasonable tool name like "evaluate" or "run_script".
+
+
+---
+
+
+
+
+
+---
+
+Tool user-blender-evaluate was not found. The list of all available MCP servers are included in C:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager/mcps. Please list and read the relevant MCP servers carefully before using call_mcp_tool again.
+
+---
+
+Thought Process
+
+I need to read the MCP configuration to find the correct server and tool names.
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **glob_file_search** • Searched codebase "undefined" • **0** result in **1** directory
+
+Directory: **T:\Dynamic-Link-Manager** (0 file)
+
+
+
+
+---
+
+Thought Process
+
+Let me try list_dir or read the project root. The error said "the list of all available MCP servers are included in C:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager/mcps" - so there's an "mcps" file or folder.
+
+
+---
+
+
+
+
+
+---
+
+File not found
+
+---
+
+
+
+Tool use: **glob_file_search** • Searched codebase "undefined" • **0** result in **1** directory
+
+Directory: **T:\Dynamic-Link-Manager** (0 file)
+
+
+
+
+---
+
+Thought Process
+
+The mcps path might be different. Let me focus on fixing the code. The user said the original base body is there - so the issue might be:
+
+1. descendants(orig) - we iterate bpy.data.objects and add ob if ob's parent chain leads to armature. So we only add direct descendants (ob.parent == armature) or descendants through a chain. But wait - we add ob when we find p == armature in the chain. So we're adding any object that has the armature in its parent chain. So the base body mesh could be parented to the armature directly or to another object that is under the armature. So we should find it.
+
+2. _find_base_body requires: ob in descendants_iter, type MESH, name has "body" and "base", and has an ARMATURE modifier with m.object == armature. So the original base body must have an armature modifier pointing to the original armature. Maybe the original's base body has the modifier pointing to a different armature? Or the modifier is there but we're iterating in wrong order?
+
+3. Maybe the issue is we're iterating orig_descendants which is a list(descendants(orig)) - and descendants returns a set. So we have a set of objects. When we _find_base_body(orig, orig_descendants), we iterate over that set. So we should find the first mesh that has body+base and armature mod to orig. Unless - the original character's base body is not in the same scene's bpy.data.objects? When you link a collection, the objects are in bpy.data.objects. So they should be there.
+
+4. One more possibility: when we have two characters (Hailey and Eddy), each has an armature. So when we call run_step_6(Eddy, Eddy.001, ...), orig = Eddy. Eddy's descendants - are they the objects that have Eddy in their parent chain? So we need objects that are parented (directly or indirectly) to Eddy_Rigify. In a linked character, the structure might be: Eddy_Rigify (armature) -> some root bone or container -> ... -> CC_Base_Body.001. So the mesh might be parented to an Empty or to a bone. So the parent of CC_Base_Body.001 might be an Empty, and that Empty's parent might be Eddy_Rigify. So when we do "for ob in bpy.data.objects, p = ob.parent, while p: if p == armature: out.add(ob)", we'd add CC_Base_Body.001 to the set. Good.
+
+Let me look at the descendants logic again. We have:
+for ob in bpy.data.objects:
+ p = ob.parent
+ while p:
+ if p == armature:
+ out.add(ob)
+ break
+ p = p.parent
+
+So we only add ob if ob's parent chain eventually reaches armature. So the immediate parent of ob could be something else. Good. So we should get all descendants.
+
+The only remaining issue could be the armature modifier. _find_base_body checks: for m in ob.modifiers: if m.type == 'ARMATURE' and m.object == armature: return ob. So we need the base body mesh to have an Armature modifier whose object is the armature. In Rigify, the body mesh typically has an Armature modifier pointing to the rig. So that should be correct.
+
+Unless - when the character is linked, the armature modifier on the original's base body might point to the armature by name or by reference. If it's by reference and the armature in the scene is the same object, it should work. If the original base body is from the linked file, its modifier might point to an armature that is also from the linked file - and that armature might be "Eddy_Rigify" in the linked data, which when instantiated in the scene could be the same bpy.data.objects["Eddy_Rigify"]. So m.object == orig should work.
+
+Let me add more robust finding: maybe the original base body doesn't have the armature modifier in the same way (e.g. modifier could be further down the stack). Or we could match by name convention: if we're processing rep base body "CC_Base_Body.002", the original might be "CC_Base_Body" - so we could find orig base body by looking for a mesh with similar base name that is in orig's hierarchy. Actually, a simpler fix: when we have multiple base bodies in rep_descendants (e.g. two characters in one scene?), we need to match each rep base body to the correct orig base body. Currently we have a single orig_base for the whole run. So when we process the first rep base body we use orig_base. When we process the second rep base body we still use the same orig_base. So if we're running step 6 once per character (orig=Eddy, rep=Eddy.001), we only have one rep base body per call. So orig_base should be Eddy's base body. So we're good.
+
+Let me re-read the _find_base_body logic. We do "for m in ob.modifiers: if m.type == 'ARMATURE' and m.object == armature: return ob" - but we need to "break" out of the inner loop and then return. Actually we have:
+if ob.modifiers:
+ for m in ob.modifiers:
+ if m.type == "ARMATURE" and m.object == armature:
+ return ob
+else:
+ continue
+So if the mesh has no modifiers we "continue" (but we're in the outer for loop - wait, the "else" is for the "for m in ob.modifiers" loop. So in Python, for/else: the else runs if the for completed without break. So we have:
+for m in ob.modifiers:
+ if m.type == "ARMATURE" and m.object == armature:
+ return ob
+else:
+ continue
+So if we don't find an armature modifier pointing to our armature, we "continue" to the next ob. So we need the mesh to have at least one modifier that is ARMATURE with object == armature. That's correct.
+
+Maybe the issue is that the original base body's armature modifier points to the armature but the armature is in a different context? Like when we have linked data, m.object might be None or a different object? Let me add a fallback: if we can't find by armature modifier, find by name - the original base body might be named "CC_Base_Body" and the rep "CC_Base_Body.002". So we could find the mesh that is in orig descendants and has the "base" name without the .001/.002 suffix. Or we could try matching by stripping the suffix from rep object name and looking for an object with that base name in orig descendants.
+
+I'll add a fallback in _find_base_body: if no mesh is found with the armature modifier, try to find a mesh in descendants that has "body" and "base" in the name (same as now) but without requiring the armature modifier - and prefer the one whose name matches the rep's base name (e.g. CC_Base_Body for CC_Base_Body.002). That way we still require body+base in name and being in the hierarchy, but we don't require the modifier in case the linked original has a different modifier setup.
+
+
+---
+
+
+Checking the Blender MCP config and improving base-body finding logic. Inspecting the full flow:
+
+
+---
+
+
+
+Tool use: **ripgrep_raw_search**
+
+
+Parameters:
+
+```json
+{
+ "pattern": "blender|mcp",
+ "path": "T:\\Dynamic-Link-Manager",
+ "glob": "*.json",
+ "caseInsensitive": false
+}
+```
+Additional data:
+
+```json
+{
+ "isPruned": true,
+ "pattern": "blender|mcp",
+ "path": "T:\\Dynamic-Link-Manager",
+ "outputMode": "content",
+ "totalFiles": 0,
+ "totalMatches": 0,
+ "topFiles": []
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+I don't have access to the Blender MCP (the mcps path wasn't found in the project - it might be in .cursor or user config). I'll make the original base body finding more robust:
+
+1. **Fallback when armature modifier doesn't match**: For linked characters, the modifier's `m.object` might not equal `armature` (e.g. if it's the same rig from the library but identity comparison fails, or the modifier references by name). So add a fallback: find any mesh in orig_descendants with "body" and "base" in the name. If there's only one, use it. If there are multiple, try to match by base name (e.g. rep is CC_Base_Body.002 → look for CC_Base_Body or similar in orig).
+
+2. **Match rep to orig by name**: When we have the rep base body `ob` (e.g. CC_Base_Body.002), we can find the corresponding orig by name: strip .001/.002/.003 from the rep name and look for that base name in orig descendants. So for CC_Base_Body.002 we'd look for CC_Base_Body or CC_Base_Body.001 in orig's descendants. That way we're matching "which character's base body" by naming convention.
+
+Let me implement: (1) In run_step_6, don't use a single orig_base for all rep base bodies. Instead, for each rep base body `ob`, find the matching orig base body by: first try _find_base_body(orig, orig_descendants). If that returns something, use it. If not, try finding by name: get "base name" from ob.name (e.g. CC_Base_Body.002 → CC_Base_Body), then in orig_descendants find a MESH with body+base in name and (name == base_name or name.startswith(base_name)). (2) Also relax _find_base_body to have a fallback: if no mesh has armature modifier pointing to armature, return the first mesh with body+base in name in descendants (so we require hierarchy + name, not modifier). That handles linked characters where the modifier might not match.
+
+
+---
+
+
+Making original base-body finding more robust: adding a fallback when the armature modifier doesn't match (common with linked data) and matching by rep base-body name.
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+
+Now I need to update run_step_6 to pass rep_base_name when finding orig base body. But we're iterating rep_descendants and for each rep base body we want the matching orig. So we should find orig_base per rep base body, using the rep base body's name. Let me update the call: compute orig_descendants once, then inside the loop for each rep base body `ob`, call _find_base_body(orig, orig_descendants, rep_base_name=ob.name).
+
+
+---
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+
+---
+
+Thought Process
+
+Now I need to fix the "else: continue" - that was attached to the "if ob.modifiers" for loop. Let me check - we have:
+if ob.modifiers:
+ for m in ob.modifiers:
+ if m.type == "ARMATURE" and m.object == rep:
+ break
+ else:
+ continue
+So the else is for the for-loop: if we never hit break (no armature mod to rep), we continue. Good. So the logic is correct. Let me verify the full loop and the debug message that says "no orig base body" - we now compute orig_base inside the loop so we need to make sure the debug and copy block still work. Let me read the file again to ensure we didn't break anything.
+
+
+---
+
+
+
+
+
+---
+
+
+Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+
+
+
+---
+
+Thought Process
+
+There's a small bug: when we don't have ob.data.shape_keys we never run the "no orig base body" debug. But more importantly, when orig_base is None we print the debug but we're inside "if ob.data.shape_keys" - so we only print when the rep has shape_keys. If rep has shape_keys but orig_base is None we print "no orig base body found". Good.
+
+One more thing: the copy runs after we've overridden the mesh and Key. So we're reading from orig_base.data.shape_keys.key_blocks[].value. For a linked original, that might be read-only or might return 0. Let me check - when the original is linked, orig_base.data could be linked mesh data. Reading .value from a linked Key block should work in Blender - it's a property. So we should get the correct value. The issue was finding orig_base. Now we have a fallback. Let me also add a debug line that prints how many orig_descendants we have and how many candidates we found, so the user can see what's happening. Actually that might be too noisy. Let me leave the current debug.
+
+Let me double-check the name matching in _find_base_body. rep_base_name could be "CC_Base_Body.002". We do base = rep_base_name.rsplit(".", 1)[0] so base = "CC_Base_Body.001" if we have "CC_Base_Body.001.002"? No - rsplit(".", 1) gives one split from the right: "CC_Base_Body.002" -> ["CC_Base_Body", "002"], so base = "CC_Base_Body". Good. Then we look for ob.name == base or ob.name.startswith(base + ".") or ob.data.name == base. So we'd match CC_Base_Body or CC_Base_Body.001 etc. Good.
+
+
+---
+
+
+Verifying name fallback: with rep "CC_Base_Body.002", base is "CC_Base_Body"; orig could be "CC_Base_Body" or "CC_Base_Body.001".
+
+
+---
+
+
+
+Tool use: **read_lints**
+
+
+Parameters:
+
+```json
+{
+ "paths": [
+ "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ ]
+}
+```
+Result:
+
+```json
+{
+ "linterErrorsByFile": [
+ {
+ "relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
+ }
+ ]
+}
+```
+Status: **completed**
+
+
+
+
+
+---
+
+Thought Process
+
+Good. I couldn't use the Blender MCP (server config not in project or different name). I've made the finding logic more robust:
+
+1. _base_body_name_match(ob) - shared check for "body" + "base" in name.
+2. _find_base_body now:
+ - First tries modifier match (armature modifier pointing to armature).
+ - Builds candidates list of all meshes in descendants with body+base in name.
+ - If no modifier match: if rep_base_name given, pick candidate whose name matches base (e.g. CC_Base_Body.002 → CC_Base_Body) or first candidate.
+3. run_step_6 finds orig_base per rep base body using rep's name (ob.name) so we match the right character when multiple are in the scene.
+
+This should find the original base body even when the armature modifier doesn't match (linked data) by using hierarchy + name.
+
+
+---
+
+
+I couldn’t use the Blender MCP from here (the tool reported the server config isn’t in this project), so the code was updated to make finding the original base body more reliable.
+
+**Changes:**
+
+1. **`_base_body_name_match(ob)`**
+ Single helper: mesh with "body" and "base" in name (used for both orig and rep).
+
+2. **`_find_base_body(armature, descendants_iter, rep_base_name=None)`**
+ - Still prefers a mesh that has an Armature modifier with `m.object == armature`.
+ - If none match, it builds a **candidates** list of all meshes in `descendants_iter` that pass `_base_body_name_match`.
+ - If `rep_base_name` is given (e.g. `"CC_Base_Body.002"`), it picks a candidate by name: base name is the part before the last dot (`"CC_Base_Body"`), then it looks for a candidate with `name == base`, `name.startswith(base + ".")`, or `data.name == base`.
+ - If there’s no name match, it returns the first candidate.
+ So the original is found even when the modifier doesn’t point to the armature (e.g. linked data).
+
+3. **`run_step_6`**
+ - No single `orig_base` for the whole run.
+ - For each replacement base body `ob`, it calls `_find_base_body(orig, orig_descendants, rep_base_name=ob.name)` so the original is chosen per rep using the rep’s name (e.g. rep `CC_Base_Body.002` → orig `CC_Base_Body` or `CC_Base_Body.001`).
+
+After reloading the add-on and running the step again, the console should show either `shapekey values: copied X/Y from ` or the existing “no orig base body” / “orig base body has no shape_keys” messages. If you can run the Blender MCP from your side (e.g. list objects under each armature and their modifiers), we can double-check the hierarchy and modifier setup.
+
+---
+
+_**User**_
+
+@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt it's printing that it still isn't finding it.
+
+---
+
+_**Agent (model default, mode Agent)**_
+
+
+
+---
+
+
+
+---
+
+
+
+Tool use: **read_file_v2**
+
+Status: **loading**
+
+
+
+
+
+---
+
+
+
+Tool use: **read_file_v2**
+
+Status: **loading**
+
+
+
+
+
+---
+
---
@@ -6216,3 +7178,47 @@ Status: **loading**
---
+
+
+Tool use: **read_file_v2**
+
+Status: **loading**
+
+
+
+
+
+---
+
+
+
+---
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+Status: **loading**
+
+
+
+
+
+---
+
+
+
+Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py
+
+Status: **loading**
+
+
+
+
+
+---
+
diff --git a/ops/migrator.py b/ops/migrator.py
index c5122ff..03824ee 100644
--- a/ops/migrator.py
+++ b/ops/migrator.py
@@ -7,7 +7,7 @@
import bpy
-from ..utils import descendants
+from ..utils import descendants, collection_containing_armature
def get_pair_manual(context):
@@ -151,13 +151,64 @@ def run_step_5(orig, rep, rep_descendants, orig_to_rep):
m.object = rep
+def _base_body_name_match(ob):
+ """True if object looks like the base body mesh (MESH, name has body+base)."""
+ if ob.type != "MESH":
+ return False
+ name_lower = (ob.name + " " + (ob.data.name if ob.data else "")).lower()
+ return "body" in name_lower and "base" in name_lower
+
+
+def _objects_in_collection_recursive(coll):
+ """Yield all objects in collection and nested collections."""
+ for ob in coll.objects:
+ yield ob
+ for child in coll.children:
+ yield from _objects_in_collection_recursive(child)
+
+
+def _find_base_body(armature, descendants_iter, rep_base_name=None):
+ """Return the base body mesh: in descendants (armature mod), or in armature's collection(s), matched by name."""
+ def gather_candidates(ob_iter):
+ candidates = []
+ for ob in ob_iter:
+ if not _base_body_name_match(ob):
+ continue
+ if ob.modifiers:
+ for m in ob.modifiers:
+ if m.type == "ARMATURE" and m.object == armature:
+ return ob, candidates
+ candidates.append(ob)
+ return None, candidates
+
+ found, candidates = gather_candidates(descendants_iter)
+ if found:
+ return found
+ # Fallback: base body may be in same collection as armature but not parented to it (e.g. linked).
+ if not candidates:
+ for coll in [collection_containing_armature(armature)] + list(getattr(armature, "users_collection", []) or []):
+ if not coll:
+ continue
+ found, candidates = gather_candidates(_objects_in_collection_recursive(coll))
+ if found:
+ return found
+ if candidates:
+ break
+ if not candidates:
+ return None
+ if rep_base_name:
+ base = rep_base_name.rsplit(".", 1)[0] if "." in rep_base_name else rep_base_name
+ for ob in candidates:
+ if ob.name == base or ob.name.startswith(base + ".") or (ob.data and ob.data.name == base):
+ return ob
+ return candidates[0]
+
+
def run_step_6(orig, rep, rep_descendants, context=None):
- """Replacement base body: library override (fully editable when context given), then shape-key action."""
+ """Replacement base body: library override (fully editable when context given), copy shapekey values, then shape-key action."""
+ orig_descendants = list(descendants(orig))
for ob in list(rep_descendants):
- if ob.type != "MESH":
- continue
- name_lower = (ob.name + " " + (ob.data.name if ob.data else "")).lower()
- if "body" not in name_lower or "base" not in name_lower:
+ if not _base_body_name_match(ob):
continue
if ob.modifiers:
for m in ob.modifiers:
@@ -165,6 +216,7 @@ def run_step_6(orig, rep, rep_descendants, context=None):
break
else:
continue
+ orig_base = _find_base_body(orig, orig_descendants, rep_base_name=ob.name)
# Debug: base body mesh state before override handling.
_lib = getattr(ob.data, "library", None)
_ol = getattr(ob.data, "override_library", None)
@@ -211,6 +263,29 @@ def run_step_6(orig, rep, rep_descendants, context=None):
_sys2 = getattr(_ol2, "is_system_override", None) if _ol2 else None
print(f"[DLM step6] {ob.name} after: override={_ol2 is not None}, is_system_override={_sys2} (False=editable)")
if ob.data.shape_keys:
+ # Ensure we can write shape key values: override the Key block if it is linked.
+ sk = ob.data.shape_keys
+ if getattr(sk, "library", None):
+ try:
+ sk.override_create(remap_local_usages=True)
+ except Exception as e:
+ print(f"[DLM step6] {ob.name} shape_keys.override_create: {e}")
+ # Copy shape key values from original base body to replacement (by matching key name).
+ if orig_base and orig_base.data.shape_keys:
+ rep_blocks = ob.data.shape_keys.key_blocks
+ orig_blocks = orig_base.data.shape_keys.key_blocks
+ n_copied = 0
+ for orig_key in orig_blocks:
+ rep_key = rep_blocks.get(orig_key.name)
+ if rep_key is not None:
+ rep_key.value = orig_key.value
+ n_copied += 1
+ print(f"[DLM step6] {ob.name} shapekey values: copied {n_copied}/{len(orig_blocks)} from {orig_base.name}")
+ else:
+ if not orig_base:
+ print(f"[DLM step6] {ob.name} no orig base body found for armature {orig.name}")
+ elif not orig_base.data.shape_keys:
+ print(f"[DLM step6] {ob.name} orig base body has no shape_keys")
if not ob.data.shape_keys.animation_data:
ob.data.shape_keys.animation_data_create()
body_name = ob.name