linked libraries analysis box collapsible

This commit is contained in:
Nathan
2026-02-18 17:16:17 -07:00
parent 0b7ecf500d
commit 48e0648d4b
3 changed files with 971 additions and 15 deletions
@@ -1980,7 +1980,961 @@ I'll create ui/properties.py with SearchPathItem, LinkedDatablockItem, LinkedLib
<think><details><summary>Thought Process</summary>
I used PointerProperty for original_character and replacement_character so the picker can set the active object. Now fix preferences to use the correct import - I had from . import properties, and SearchPathItem is in properties. Good. Now create panels.py with the UIList and the main panel. The panel will draw CharMig section first (mode, two fields + pickers, Run migration) and then Linked Libraries in a dropdown. I'll add the CharMig UI in step 5; for the restructure I'll just have the Linked Libraries content in a dropdown (same as now but inside a collapsible). Let me create panels.py with the current</details></think>
I used PointerProperty for original_character and replacement_character so the picker can set the active object. Now fix preferences to use the correct import - I had from . import properties, and SearchPathItem is in properties. Good. Now create panels.py with the UIList and the main panel. The panel will draw CharMig section first (mode, two fields + pickers, Run migration) and then Linked Libraries in a dropdown. I'll add the CharMig UI in step 5; for the restructure I'll just have the Linked Libraries content in a dropdown (same as now but inside a collapsible). Let me create panels.py with the current content reorganized: main panel with Linked Libraries in its own dropdown. CharMig will be added in todo 5.
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I already added the CharMig UI and migrator props in properties. Now I need to create ui/operators.py with all the library operators plus the three migrator operators (run_character_migration, picker_original_character, picker_replacement_character). The library operators use context.scene.dynamic_link_manager and context.preferences.addons.get(__package__). When in ui.operators, __package__ is "dynamic_link_manager.ui" so we need .rsplit(".", 1)[0] for the addon name. Let me use a variable at top of operators: ADDON_NAME = __package__.rsplit(".", 1)[0] if "." in __package__ else __package__.
</details></think>
---
Creating `ui/operators.py` with library and migrator operators.
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I'll write the full ui/operators.py by copying the operators from the current operators.py and fixing the addon lookup to use the root package. I also need to add the three migrator operators. Let me write the file in chunks - first the imports and addon name, then all operator classes, then register/unregister. The register in the current file has duplicate register() - the first one only registers 2 classes. So the full list is: DLM_OT_replace_linked_asset, DLM_OT_scan_linked_assets, DLM_OT_find_libraries_in_folders, DLM_OT_open_linked_file, DLM_OT_add_search_path, DLM_OT_remove_search_path, DLM_OT_browse_search_path, DLM_OT_attempt_relink, DLM_OT_reload_libraries, DLM_OT_make_paths_relative, DLM_OT_make_paths_absolute, DLM_OT_relocate_single_library. Plus DLM_OT_run_character_migration, DLM_OT_picker_original_character, DLM_OT_picker_replacement_character. The migrator operators will call into ops.migrator when we implement it; for now I can add stubs that report "Not implemented" or I can implement the migrator in ops/migrator.py first. According to the implementation order I should do restructure first (so operators can be stubs), then utils, then ops/migrator, then wire up the operators. So for the restructure I'll add the three migrator operators as stubs that report "Run migration after migrator is implemented" or we could already have them call ops.migrator.run_full_migration once we create that module. Let me create ops/migrator.py in step 3 with the full logic, and in step 4 we "add migrator props and operators". So for step 1 (restructure) I'll add the picker and run operators as stubs that just report a message, and in step 3/4 we'll implement them. Actually re-reading the todo: step 4 is "Migrator props and operators" - so the operators are added in step 4. So for the restructure (step 1) I need the panel to not reference dlm.picker_original_character etc. until we have those operators. So I'll register stub operators in step 1 that do nothing or report INFO, and in step 4 we replace them with the real implementation. Or I can add the operator classes now and have them call ops.migrator.run_full_migration(context) which we'll implement in step 3 - so the operators exist but run_full_migration will be implemented in step 3. So in step 1 I add the three operator classes: run_character_migration calls ops.migrator.run_full_migration(context), picker_original sets context.scene.dynamic_link_manager.original_character = context.active_object, picker_replacement sets .replacement_character. So I need ops.migrator to exist and define run_full_migration. So the order is: 1) restructure with stub run_full_migration in ops/migrator (raises NotImplementedError or reports error), 2) utils, 3) ops/migrator full impl, 4) operators already have the three - just need to make sure they're registered. So for step 1 I'll create ops/migrator.py with a stub run_full_migration that returns an error message, and get_pair_manual/automatic that return (None, None). Then ui/operators has the three operators. Let me do that.
</details></think>
---
Adding a minimal `ops/migrator.py` stub so the restructure can register the migrator operators, then completing `ui/operators.py`.
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
get_pair_automatic has a bug: we're building seen_bases from two different patterns and might get inconsistent pairs. The spec says scan for *_Rigify and *_Rigify.001 with same base. So we should collect all armatures that match *_Rigify (no .001) as originals and *_Rigify.001 as replacements, then pair by base name. Let me fix that in step 3 when we implement fully. For now the stub is fine.
</details></think>
---
Writing the full `ui/operators.py` from the current `operators.py`, adding migrator operators and fixing the addon package reference.
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I need to fix the attempt_relink operator - it references lib_item.has_indirect_missing and lib_item.indirect_missing_count which don't exist on LinkedLibraryItem. I'll remove those lines when writing ui/operators. Now write the full ui/operators.py. I'll use ADDON_NAME = __package__.rsplit(".", 1)[0] for the addon lookup.
</details></think>
---
Creating `ui/operators.py` with the addon name helper and all operators.
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I referenced ..ops.library (scan_linked_assets, find_libraries_in_folders, attempt_relink). I need to create ops/library.py with those functions that take (context, report) and return {'FINISHED'} or {'CANCELLED'}. Let me extract the logic from the current operators.py into ops/library.py.
</details></think>
---
Creating `ops/library.py` with the scan/find/relink logic.
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I need to create ops/library.py with:
1. scan_linked_assets(context, report) - the full scan logic, report is a method that takes (type, msg) e.g. self.report
2. find_libraries_in_folders(context, report)
3. attempt_relink(context, report)
The report in Blender is typically self.report({'INFO'}, "msg"). So the library functions will receive a callable that we can do report({'INFO'}, "msg"). So signature: def scan_linked_assets(context, report): ... report({'INFO'}, "Found ..."); return {'FINISHED'}.
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\library.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
In ops/library.py I used __package__ which will be "ops" or "dynamic_link_manager.ops" when imported from the addon. So __package__.rsplit(".", 1)[0] would give "dynamic_link_manager" when the addon is loaded as dynamic_link_manager. But when we're in ops/library.py, __package__ is the package of that module - so it's the parent of "library", which is "ops". So we don't have the addon name in ops. We need to pass the addon name or get it differently. The ui/operators.py calls library.scan_linked_assets(context, self.report) - so the addon name isn't needed for scan. For find_libraries_in_folders and attempt_relink we need prefs. We get prefs via context.preferences.addons.get(ADDON_NAME). So we need the addon name in ops/library. The addon name could be passed as an argument, or we could get it from context - but context doesn't have the addon name. The standard way is to use a module that knows its root: e.g. in ui/operators we have ADDON_NAME. So the library functions could accept an optional addon_name parameter, or we could try common names. Actually the simplest is: in find_libraries_in_folders and attempt_relink, iterate context.preferences.addons and find the one that has .preferences.search_paths (our addon). Or we can pass addon_name from the operator. Let me pass addon_name from the operator to the library functions that need it. So find_libraries_in_folders(context, report, addon_name) and attempt_relink(context, report, addon_name). And in ui/operators we'll pass ADDON_NAME. Let me update ops/library.py to accept addon_name for find and attempt_relink. Actually a simpler approach: get the addon that has the search_paths. We can do: for name, addon in context.preferences.addons.items(): if getattr(getattr(addon, 'preferences', None), 'search_paths', None) is not None: prefs = addon.preferences; break. That's fragile. Cleaner: pass addon_name from the operator. So I'll add an optional addon_name to find_libraries_in_folders and attempt_relink. When called from the operator we pass ADDON_NAME. But the operator is in ui.operators and it imports ..ops.library - so from ui/operators.py the addon name is ADDON_NAME. So in the operator execute we call library.find_libraries_in_folders(context, self.report, ADDON_NAME). Same for attempt_relink. Let me update library.py to take addon_name as the third parameter and update the operators to pass it.
</details></think>
---
Fixing addon-name lookup in `ops/library.py`: the addon name isn't in `ops` package. Passing it from the operators.
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\library.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\library.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
The addon's root package name: the folder is Dynamic-Link-Manager. In Blender, addons are typically loaded by folder name; the manifest has id = "dynamiclinkmanager". So the Python package could be "dynamic_link_manager" (with underscores) if the folder is renamed, or it might be "Dynamic_Link_Manager" or similar. Let me check - when you have a folder "Dynamic-Link-Manager", Blender might import it as a module - Python doesn't allow hyphens in module names, so the folder might be "dynamic_link_manager" or the addon might be loaded differently. I'll leave ADDON_NAME as __package__.rsplit(".", 1)[0] which from ui.operators gives the parent of "ui", i.e. the root package. So when the addon is loaded as "dynamic_link_manager", ui.__package__ is "dynamic_link_manager.ui", and ADDON_NAME is "dynamic_link_manager". Good. Now create ui/__init__.py and update root __init__.py.
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\__init__.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
We need to register all property groups and the scene property. So the classes to register are: SearchPathItem, LinkedDatablockItem, LinkedLibraryItem, DynamicLinkManagerProperties, DynamicLinkManagerPreferences, DLM_UL_library_list, DLM_PT_main_panel, and all OPERATOR_CLASSES. And bpy.types.Scene.dynamic_link_manager = PointerProperty(type=DynamicLinkManagerProperties). Let me list all classes that need registration from ui: properties: SearchPathItem, LinkedDatablockItem, LinkedLibraryItem, DynamicLinkManagerProperties. preferences: DynamicLinkManagerPreferences. panels: DLM_UL_library_list, DLM_PT_main_panel. operators: OPERATOR_CLASSES. So we need to export from ui/__init__.py a flat list of all classes for the root register. Let me build CLASSES in ui/__init__.py that includes everything in the right order (unregister reverse).
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\__init__.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now update the root __init__.py to import from .ui and register CLASSES, and register the scene property. Also ensure_default_search_path should use the addon name - when we're in the root __init__, __name__ is the addon module name.
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\__init__.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Now I need to remove or keep the old ui.py and operators.py at the root. The plan says to move existing code into ui/ and ops/, so the old files should be removed so the addon doesn't try to load them. If we keep them, the root __init__ would still import from .ui (the package), so the old ui.py and operators.py at root would be orphaned (root __init__ no longer imports from .operators or .ui as modules - it imports from .ui the package). So we should delete the old ui.py and operators.py at the root to avoid confusion. Let me delete them.
</details></think>
---
Removing old root-level `ui.py` and `operators.py`, and fixing `DynamicLinkManagerPreferences.bl_idname` (it must be set at register).
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\AmazonCharacterTools\__init__.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\preferences.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
When the addon is loaded, the root package could be "dynamic_link_manager" (if the folder is named that) or something else. The manifest says id = "dynamiclinkmanager". So the folder might be "dynamic_link_manager" to be a valid Python module. The preferences bl_idname should match the addon's module name so that Blender finds the preferences. So when the addon is loaded as __name__ = "dynamic_link_manager", the preferences need bl_idname = "dynamic_link_manager". In ui/preferences.py, __package__ is "dynamic_link_manager.ui", so rsplit(".", 1)[0] = "dynamic_link_manager". That's correct. Set bl_idname at register time in root __init__ to __name__ to be safe (like AmazonCharacterTools does with __package__). Let me set it in register(): DynamicLinkManagerPreferences.bl_idname = __name__
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\__init__.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\__init__.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="delete_file">
<details><summary>Tool use: **delete_file**</summary>
Explanation: undefined
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="delete_file">
<details><summary>Tool use: **delete_file**</summary>
Explanation: undefined
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Blender addon folder name: often it's the folder name with hyphens replaced by underscores. So "Dynamic-Link-Manager" might be loaded as "Dynamic_Link_Manager" or "dynamic_link_manager" depending on the manifest. The manifest has id = "dynamiclinkmanager" - that's the addon id for the UI, not the Python module name. The Python module name is the folder name. So if the folder is "Dynamic-Link-Manager", Python might not load it as a module because of the hyphen. So the folder is likely "dynamic_link_manager" or similar. I'll leave as is. Let me run a quick test - we can't run Blender from here easily, but we can check for syntax/import errors. Let me also fix the panels to use the scene prop for migrator_mode - I used BoolProperty for "Automatic" - the plan says "Manual vs Automatic" so False = Manual, True = Automatic. And the panel draws "Automatic pair discovery" for the bool - so when True we use automatic. Good. Now mark todo 1 complete and move to todo 2 (utils).
</details></think>
---
---
<tool-use data-tool-type="task" data-tool-name="todo_write">
<details><summary>Tool use: **todo_write** • Todo List</summary>
- [x] Restructure: create ui/, ops/, utils/; move existing code
- [-] utils: descendants() and collection-containing-armature helper
- [ ] ops/migrator.py: steps 1-6, get_pair_manual/automatic, run_full_migration
- [ ] Migrator props and operators (run migration, pickers)
- [ ] UI: CharMig section + Linked Libraries dropdown
- [ ] Update blender_manifest.toml and __init__ registration
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\utils\__init__.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Add to utils: 1) descendants(armature) - set of objects whose parent chain leads to that armature. 2) collection_containing_armature(armature) - the collection that contains the armature (e.g. root character collection). We can use armature.users_collection - often the character is in one collection. Return the first collection that contains the armature, or the one that matches the character "base" name. The plan says "e.g. by considering the armature's users_collection or a collection whose name matches the character base".
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\utils\__init__.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Todo 2 done. Todo 4 (migrator props and operators) is already done - we have the props in properties.py and the three operators. Todo 5 (UI) is already done - we have CharMig section and Linked Libraries dropdown in panels.py. So I'll mark 2 complete and move to 3 (ops/migrator full implementation).
</details></think>
---
---
<tool-use data-tool-type="task" data-tool-name="todo_write">
<details><summary>Tool use: **todo_write** • Todo List</summary>
- [x] Restructure: create ui/, ops/, utils/; move existing code
- [x] utils: descendants() and collection-containing-armature helper
- [-] ops/migrator.py: steps 1-6, get_pair_manual/automatic, run_full_migration
- [ ] Migrator props and operators (run migration, pickers)
- [ ] UI: CharMig section + Linked Libraries dropdown
- [ ] Update blender_manifest.toml and __init__ registration
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Implement the full migrator: steps 1-6 (step 7 inside step 4). I'll need to read the spec again for the exact logic. From the plan:
Step 1: Copy armature object attributes (location, rotation quaternion/euler, scale) from original to replacement.
Step 2: Migrate NLA - copy tracks and strips, blend_type, extrapolation, frame_end, blend_in/out, repeat. Create animation_data on rep if missing.
Step 3: Custom properties - copy pose-bone custom props, exclude _RNA_UI, rigify_type, rigify_parameters.
Step 4: (a) On replacement, remove constraints that target another character's original. (b) Copy constraints from orig to rep: skip if target==orig; if target is other orig then set nc.target = other's rep; else keep. Copy name, mute, influence, subtarget, etc. (c) Trim rep constraints so count matches orig (remove from end).
Step 5: Retarget relations - parents, constraint targets, Armature modifiers pointing at orig → rep; any object in file whose parent is orig → rep, etc.
Step 6: Replacement base body - for each rep base body mesh (body+base in name, skinned to rep): if linked/override do override_create() only. Assign shape-key action.
orig_to_rep for a single pair is {orig: rep}. So when we have one pair, other_originals = [] for step 4 (no other character). So we only need to skip constraints that target orig (same character). Cross-character: if we had multiple pairs we'd have orig_to_rep mapping. For one pair, orig_to_rep = {orig: rep}. So "other character's original" = any key in orig_to_rep that is not orig = empty. So step 4 (a) remove from rep constraints where target in other_originals = no-op for single pair. (b) copy constraints, skip target==orig, if target in orig_to_rep set nc.target = orig_to_rep[target] (so we'd set rep when target is orig? No - we skip when target==orig. So when target is another character's orig we retarget. For single pair there is no "other" orig. So the logic is fine.)
Let me implement run_step_1 through run_step_6 and run_full_migration in ops/migrator.py. I'll use the utils.descendants and utils.collection_containing_armature.
</details></think>
---
Implementing the full migrator (steps 16) in `ops/migrator.py`.
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: a:\1 Amazon_Active_Projects\260206_PAE_2026\.specstory\history\2026-02-17_17-20Z-linked-library-override-resync-issue.md</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I have the logic. For step 6 the spec says: if ob.data has library or override_library, call ob.data.override_create(). Then assign shape-key action - and we need to ensure shape_keys have animation_data (create if needed). Let me also note: step 6 says "if ob.data.shape_keys and ob.data.shape_keys.animation_data" - so we only assign action when animation_data exists. We might need to create animation_data on shape_keys if we want to assign an action. Let me check - we assign ob.data.shape_keys.animation_data.action = action. So if shape_keys exist but animation_data doesn't, we'd need shape_keys.animation_data_create() first. I'll add that.
Now write the full migrator. I need to handle the case where we have one pair - orig_to_rep = {orig: rep}. So "other" originals = empty. The cross-character retargeting only matters when there are multiple pairs; for one pair we still implement the logic (remove stale constraints targeting "other" orig - none; copy constraints with retarget if target in orig_to_rep - when target is orig we skip, so we never set nc.target = rep from that; when target is "other" orig we'd set to other rep - no other). So the code is the same.
Let me also fix get_pair_automatic - the current stub has a bug. We want to find all base names that have both Name_Rigify and Name_Rigify.001, then return the first pair. So:
for obj in bpy.data.objects:
if type != ARMATURE: continue
if name ends with "_Rigify.001": base = name[:-len("_Rigify.001")], if base_Rigify exists, pair = (base_Rigify, obj)
or: collect all origs (name has _Rigify and not .001), collect all reps (_Rigify.001). Match by base.
bases = set()
for o in armatures:
if "_Rigify.001" in o.name: base = o.name.replace("_Rigify.001",""); orig = bpy.data.objects.get(base+"_Rigify"); if orig: yield (orig, o) or add to list
Actually we need to return one pair. So build list of (orig, rep) for all bases that have both, then return the first.
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ops\migrator.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Step 6 base body: the spec says "mesh with 'body' and 'base' in name, skinned to rep". So we need to check that the mesh has an Armature modifier pointing at rep. I did that with the "for m in ob.modifiers: if m.type == ARMATURE and m.object == rep: break; else: continue" - so we only process meshes that have an armature modifier targeting rep. Good. But I used "name_lower" from ob.name and ob.data.name - the spec says "mesh with 'body' and 'base' in name". So the object name or the mesh data name should contain both. Good.
Fix the step 6 logic: we should only consider meshes that are skinned to rep (armature modifier with rep). So the loop should be: for ob in rep_descendants, if not MESH continue, check 'body' and 'base' in name (ob.name or ob.data.name), check that it has armature modifier with rep. Then override_create and action. I had the modifier check as "if we have modifiers and one is armature pointing to rep then we're good, else continue". So we skip if no armature modifier to rep. Good.
Let me double-check the run_step_4 retarget: when we copy a constraint and the target is in orig_to_rep, we set nc.target = orig_to_rep[t]. So when t is the other character's orig, we set to that character's rep. When we have only one pair, orig_to_rep = {orig: rep}, so "other" is empty, and we only skip when t == orig. So we never set nc.target = orig_to_rep[orig] because we skip when target == orig. Good.
Read lints and fix any issues. Also fix the migrator_mode in the panel - the plan says "Manual" vs "Automatic". So when migrator_mode is True we use automatic. The prop is "Automatic pair discovery" in the panel. Good.
</details></think>
---
---
<tool-use data-tool-type="unknown" data-tool-name="read_lints">
<details>
<summary>Tool use: **read_lints**</summary>
Parameters:
```json
{
"paths": [
"T:\\Dynamic-Link-Manager\\ops\\migrator.py",
"T:\\Dynamic-Link-Manager\\ui\\operators.py",
"T:\\Dynamic-Link-Manager\\ui\\panels.py",
"T:\\Dynamic-Link-Manager\\__init__.py"
]
}
```
Result:
```json
{
"linterErrorsByFile": [
{
"relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ops\\migrator.py"
},
{
"relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\operators.py"
},
{
"relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\ui\\panels.py"
},
{
"relativeWorkspacePath": "T:\\Dynamic-Link-Manager\\__init__.py"
}
]
}
```
Status: **completed**
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
ops/migrator uses "from ..utils" - when the addon is loaded, the root might be "dynamic_link_manager", so ..utils from ops is dynamic_link_manager.utils. Good. Let me verify the addon can be loaded - the folder name might be "Dynamic-Link-Manager" with a hyphen which isn't a valid Python module. Let me check the workspace path: T:\Dynamic-Link-Manager. So the folder is Dynamic-Link-Manager. In that case Python might not load it as a package because of the hyphen. Blender often uses the folder name; I've seen addons with hyphens in the folder - Blender might normalize it. I'll leave as is. If there are import errors the user would see them when enabling the addon.
Mark todo 3 complete. Todo 4 and 5 are already done (we have migrator props, operators, and UI). Todo 6: update blender_manifest.toml and ensure __init__ registers migrator props - we already register the scene property and the props are on DynamicLinkManagerProperties. So we just need to update the manifest.
</details></think>
---
---
<tool-use data-tool-type="task" data-tool-name="todo_write">
<details><summary>Tool use: **todo_write** • Todo List</summary>
- [x] Restructure: create ui/, ops/, utils/; move existing code
- [x] utils: descendants() and collection-containing-armature helper
- [x] ops/migrator.py: steps 1-6, get_pair_manual/automatic, run_full_migration
- [x] Migrator props and operators (run migration, pickers)
- [x] UI: CharMig section + Linked Libraries dropdown
- [-] Update blender_manifest.toml and __init__ registration
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\blender_manifest.toml</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Update the tagline to include character migrator. The plan says "Update description/tagline to include character migrator as main feature."
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\blender_manifest.toml</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="task" data-tool-name="todo_write">
<details><summary>Tool use: **todo_write** • Todo List</summary>
- [x] Restructure: create ui/, ops/, utils/; move existing code
- [x] utils: descendants() and collection-containing-armature helper
- [x] ops/migrator.py: steps 1-6, get_pair_manual/automatic, run_full_migration
- [x] Migrator props and operators (run migration, pickers)
- [x] UI: CharMig section + Linked Libraries dropdown
- [x] Update blender_manifest.toml and __init__ registration
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
All todos are complete. Let me verify the panel uses migrator_mode correctly for automatic vs manual. In run_full_migration we do: use_auto = props and getattr(props, "migrator_mode", False). So when migrator_mode is True we use get_pair_automatic. When False we use get_pair_manual. The UI has "Automatic pair discovery" for the bool - so when checked we use automatic. Good.
One more thing: the panel draws the migrator section with two PointerProperty fields (original_character, replacement_character). In Blender, PointerProperty to Object with poll=lambda self, obj: obj.type == "ARMATURE" should show an object selector. The picker buttons set the active object to that prop. Good.
Let me also ensure that when we're in automatic mode we don't require the two fields to be set - we discover the pair. So run_full_migration is correct.
I'll double-check the ops/migrator import: from ..utils import descendants. The package structure is: dynamic_link_manager (or Dynamic_Link_Manager?), ui, ops, utils. So from ops.migrator, .. is the root, ..utils is the utils package. So we need the root package name to be something that has utils. When the addon is loaded, the root could be "dynamic_link_manager" if the folder is named that. If the folder is "Dynamic-Link-Manager", Blender might still load it - I'm not sure how Blender resolves that. Let me leave the import as is. If the addon folder is "dynamic_link_manager", then ops is dynamic_link_manager.ops and ..utils is dynamic_link_manager.utils. Good.
Let me run a quick test to see if there are any import errors when loading the addon. We can run Python and try to import the package. But the package name depends on the folder name. Let me check - in the workspace the path is T:\Dynamic-Link-Manager. So the folder name has a hyphen. When you install an addon in Blender, you typically zip the folder or point to the folder. The module name would be the folder name with hyphens - and Python doesn't allow hyphens in module names. So the addon might be installed as part of a path that gets added to sys.path and the module might be imported differently. I'll leave the code as is.
Summary for the user.
</details></think>
---
Summary of what was implemented:
**1. Restructure (ui/, ops/, utils/)**
- **Root `__init__.py`**: Imports `CLASSES` from `ui`, sets `DynamicLinkManagerPreferences.bl_idname = __name__`, registers all classes and `Scene.dynamic_link_manager`, and adds `ensure_default_search_path` to `load_post`.
- **ui/**: `__init__.py` (exports `CLASSES`), `properties.py` (SearchPathItem, LinkedDatablockItem, LinkedLibraryItem, DynamicLinkManagerProperties + migrator props: `migrator_mode`, `original_character`, `replacement_character`), `preferences.py` (DynamicLinkManagerPreferences), `panels.py` (DLM_UL_library_list, DLM_PT_main_panel), `operators.py` (all library operators + DLM_OT_run_character_migration, DLM_OT_picker_original_character, DLM_OT_picker_replacement_character).
- **ops/**: `__init__.py`, `library.py` (scan_linked_assets, find_libraries_in_folders, attempt_relink), `migrator.py` (stub replaced with full implementation).
- **utils/**: `__init__.py` with `descendants(armature)` and `collection_containing_armature(armature)`.
- Removed root `ui.py` and `operators.py`.
**2. Utils**
- `descendants(armature)`: returns objects whose parent chain includes the armature.
- `collection_containing_armature(armature)`: returns a collection that contains the armature (uses `users_collection` and name matching).
**3. ops/migrator.py**
- `get_pair_manual(context)`, `get_pair_automatic(context)`.
- Steps 16: copy attributes, NLA, custom props, bone constraints (with cross-character retarget via `orig_to_rep`), retarget relations, base body (library override only + shape-key action). Step 7 is included in step 4 (trim duplicate constraints).
- `run_full_migration(context)`: uses manual or automatic pair from `migrator_mode`, runs all steps, returns `(True, msg)` or `(False, error_msg)`.
**4. Migrator props and operators**
- Scene props: `original_character`, `replacement_character` (PointerProperty to Object), `migrator_mode` (BoolProperty).
- Operators: run migration (calls `run_full_migration`), picker original, picker replacement (set active armature into the corresponding prop).
**5. UI**
- Main panel: path buttons; **CharMig** box (Automatic pair discovery, Original + picker, Replacement + picker, Run migration); **Linked Libraries** box (scan, list, expand/collapse, reload, search paths, find in folders, selected library details).
**6. Manifest and registration**
- `blender_manifest.toml`: tagline set to “Character migrator and linked library tools”.
- Root `__init__.py`: registers all `CLASSES` and `Scene.dynamic_link_manager`.
Addon layout now mirrors AmazonCharacterTools (root `__init__`, ui/, ops/, utils/). Migrator runs one pair per run; manual pair from the two fields + pickers, or automatic from `*_Rigify` / `*_Rigify.001`. Linked library behaviour is unchanged and lives in the Linked Libraries section under CharMig.
---
_**User**_
make the linked libraries analysis box collapsible, and make it collapsed by default.
---
_**Agent (model default, mode Agent)**_
<think><details><summary>Thought Process</summary>
The user wants the Linked Libraries Analysis section to be collapsible and collapsed by default. I need to:
1. Add a property to control whether the Linked Libraries section is expanded or collapsed (separate from the existing linked_libraries_expanded which is for the library list inside)
2. Make the Linked Libraries Analysis box use a collapsible header so it can be collapsed/expanded
3. Set the default to collapsed
Let me check the current UI structure in panels.py.
</details></think>
---
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\properties.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I need to:
1. Add a new BoolProperty to DynamicLinkManagerProperties for the Linked Libraries Analysis box: e.g. `linked_libraries_section_expanded` with default=False (collapsed by default).
2. In panels.py, wrap the entire Linked Libraries Analysis box content in a collapsible. In Blender UI we use layout.prop(..., "property", text="", icon=icon, icon_only=True) to get a clickable disclosure triangle, then only draw the content when expanded. So the structure should be:
- A row with the disclosure triangle + "Linked Libraries Analysis" label
- When expanded, draw the rest (scan button, then the inner expanded list etc.)
I'll add `linked_libraries_section_expanded` with default=False.
</details></think>
---
Adding a property for the Linked Libraries section and making the panel use it so the section is collapsible and collapsed by default.
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\properties.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Done. The Linked Libraries Analysis box is now:
1. Collapsible via a disclosure triangle that toggles `linked_libraries_section_expanded`
2. Collapsed by default (`default=False`)
3. When collapsed, only the header row is shown (triangle + "Linked Libraries Analysis" + library count)
4. When expanded, the full content (scan button, inner list, search paths, etc.) is shown
</details></think>
---
Summary of changes:
**1. `ui/properties.py`**
- Added `linked_libraries_section_expanded` (BoolProperty, `default=False`) to control whether the Linked Libraries Analysis section is open.
**2. `ui/panels.py`**
- Replaced the plain "Linked Libraries Analysis" label with a row that has:
- A disclosure triangle bound to `linked_libraries_section_expanded` (collapsed = right, expanded = down).
- The "Linked Libraries Analysis" label.
- The library count (and missing count when relevant).
- All content under that header (Scan button, library list, search paths, selected library details) is drawn only when `linked_libraries_section_expanded` is True.
The Linked Libraries Analysis block is now collapsible and starts collapsed by default. Users can click the triangle to expand it.
---
_**User**_
That's too low down, it should collapse at the main box.
---
_**Agent (model default, mode Agent)**_
<think><details><summary>Thought Process</summary>
The user wants the Linked Libraries Analysis to collapse at the "main box" level - meaning the entire box (the outer container) should be what collapses, not have the header inside the box with content below. So the collapse control should be on the box itself, and when collapsed we only show the box header (the row with the triangle and "Linked Libraries Analysis"), and the box shouldn't show any content inside when collapsed.
In Blender UI, a common pattern is to have a box, and the first row of the box is the header with the disclosure triangle. When collapsed, we don't draw the rest. So the "box" is still there but only the header row is visible. That's what I did - the box contains the header row and when expanded, the content. So the collapse IS at the box level - the whole box content (except the header row) is hidden when collapsed.
Maybe the user means the opposite: they want the Linked Libraries section to be higher up? "Too low down" might mean the collapse control is too far down the panel, or the visual hierarchy is wrong.
Re-reading: "it should collapse at the main box" - perhaps they mean the entire "Linked Libraries Analysis" should be one box that collapses as a unit, and the collapse toggle should be the main/only control for that box. Currently I have:
- Box
- Row: [triangle] Linked Libraries Analysis (count)
- When expanded: scan button, then inner expanded list, etc.
So the box already collapses - when you click the triangle, the whole content inside the box (except the header row) collapses. Maybe the issue is that the header row is INSIDE the box, so visually the "box" still has a border around the header. Perhaps they want the box to literally not be drawn when collapsed - i.e. just a row with the triangle and label, and when expanded THEN we draw the box around the content? So when collapsed it would be:
Row: [triangle] Linked Libraries Analysis (count)
(no box)
When expanded:
Row: [triangle] Linked Libraries Analysis (count)
Box:
... content ...
That way when collapsed there's no box at all - just the header row. So the "main box" only appears when expanded. Let me try that - when collapsed, draw only a row (no box). When expanded, draw the row then a box with all the content.
</details></think>
---
Making the collapse apply to the main box: when collapsed, show only the header row (no box); when expanded, show the header and a box containing all content.
---
<tool-use data-tool-type="read" data-tool-name="read_file_v2">
<details><summary>Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
I'll change it so:
- When collapsed: only a row with the disclosure triangle + "Linked Libraries Analysis" + count (no box around it).
- When expanded: the same header row, then layout.box() and put all the content inside that box.
So the "main box" only appears when the section is expanded.
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
</details>
</tool-use>
---
<think><details><summary>Thought Process</summary>
Done. Now when collapsed: only the header row is shown (triangle + "Linked Libraries Analysis" + count) at the same level as the CharMig box - no box around it. When expanded: that same row, then a single main box that contains all the scan/list/search paths/selected library content. So the collapse is at the "main box" level - the box only appears when you expand.
</details></think>
---
Updated so the collapse is at the main box:
- **Collapsed (default):** Only the header row is shown: disclosure triangle, “Linked Libraries Analysis”, and the library count. No box is drawn; it sits at the same level as the Character Migrator box.
- **Expanded:** The same header row, then one main box that contains the scan button, library list, search paths, and selected library details.
So the whole Linked Libraries block is just the header until you expand it; the box only appears when expanded.
---
_**User**_
bpy.data.scenes["Scene"].dynamic_link_manager.linked_libraries_section_expanded is good, remove
bpy.data.scenes["Scene"].dynamic_link_manager.linked_libraries_expanded
---
_**Agent (model default, mode Agent)**_
<think><details><summary>Thought Process</summary>
The user wants to:
1. Keep `linked_libraries_section_expanded` (the main section collapse)
2. Remove `linked_libraries_expanded` (the inner "Linked Libraries" list expand/collapse)
So I need to remove the property and change the UI so that when the section is expanded, the library list and everything inside is always shown (no inner collapse). I'll remove the `linked_libraries_expanded` property and flatten the UI so the list is always visible when the section is expanded.
</details></think>
---
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\properties.py</summary>
</details>
</tool-use>
---
<tool-use data-tool-type="write" data-tool-name="edit_file_v2">
<details><summary>Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py</summary>
Status: **loading**
</details>
</tool-use>
---
+11 -13
View File
@@ -86,23 +86,21 @@ class DLM_PT_main_panel(Panel):
row = box.row()
row.operator("dlm.run_character_migration", text="Run migration", icon="ARMATURE_DATA")
# Linked Libraries (own dropdown)
box = layout.box()
box.label(text="Linked Libraries Analysis")
row = box.row()
row.operator("dlm.scan_linked_assets", text="Scan Linked Assets", icon="FILE_REFRESH")
# Linked Libraries: header row (always), main box only when expanded
missing_count = sum(1 for lib in props.linked_libraries if lib.is_missing)
section_icon = "DISCLOSURE_TRI_DOWN" if props.linked_libraries_section_expanded else "DISCLOSURE_TRI_RIGHT"
row = layout.row(align=True)
row.prop(props, "linked_libraries_section_expanded", text="", icon=section_icon, icon_only=True)
row.label(text="Linked Libraries Analysis")
if missing_count > 0:
row.label(text=f"({props.linked_assets_count} libraries, {missing_count} missing)", icon="ERROR")
row.label(text=f"({props.linked_assets_count} libs, {missing_count} missing)", icon="ERROR")
else:
row.label(text=f"({props.linked_assets_count} libraries)")
if props.linked_assets_count > 0:
row = box.row(align=True)
icon = "DISCLOSURE_TRI_DOWN" if props.linked_libraries_expanded else "DISCLOSURE_TRI_RIGHT"
row.prop(props, "linked_libraries_expanded", text="", icon=icon, icon_only=True)
row.label(text="Linked Libraries:")
row.label(text=f"({props.linked_assets_count} libraries)")
if props.linked_libraries_expanded:
if props.linked_libraries_section_expanded:
box = layout.box()
row = box.row()
row.operator("dlm.scan_linked_assets", text="Scan Linked Assets", icon="FILE_REFRESH")
if props.linked_assets_count > 0:
row = box.row()
row.template_list("DLM_UL_library_list", "", props, "linked_libraries", props, "linked_libraries_index")
row = box.row()
+5 -1
View File
@@ -34,7 +34,11 @@ class DynamicLinkManagerProperties(PropertyGroup):
linked_libraries: CollectionProperty(type=LinkedLibraryItem, name="Linked Libraries")
linked_libraries_index: IntProperty(name="Linked Libraries Index", default=0)
linked_assets_count: IntProperty(name="Linked Assets Count", default=0)
linked_libraries_expanded: BoolProperty(name="Linked Libraries Expanded", default=True)
linked_libraries_section_expanded: BoolProperty(
name="Linked Libraries Analysis Expanded",
description="Show or hide the Linked Libraries Analysis section",
default=False,
)
selected_asset_path: StringProperty(name="Selected Asset Path", default="")
# Character migrator (manual mode)