From f61aee8937270363a052033630853d01ad4dbce1 Mon Sep 17 00:00:00 2001 From: Nathan Date: Tue, 31 Mar 2026 16:17:16 -0600 Subject: [PATCH] specst --- ...0-29-12Z-retargeting-focus-object-issue.md | 5262 ++++++++++++++++- .specstory/statistics.json | 60 +- 2 files changed, 5237 insertions(+), 85 deletions(-) diff --git a/.specstory/history/2026-03-13_20-29-12Z-retargeting-focus-object-issue.md b/.specstory/history/2026-03-13_20-29-12Z-retargeting-focus-object-issue.md index 89feaf0..2607f00 100644 --- a/.specstory/history/2026-03-13_20-29-12Z-retargeting-focus-object-issue.md +++ b/.specstory/history/2026-03-13_20-29-12Z-retargeting-focus-object-issue.md @@ -5325,7 +5325,6 @@ I'm implementing a recursive check to detect if a collection contains an object _**Agent (composer-2-fast)**_ - Checking the Remove Original flow: removing a collection that contains both armatures deletes both. Adding a guard so we don't remove a collection that contains the replacement armature. @@ -5356,7 +5355,6 @@ I'm implementing edge case handling for nested collections and merging with libr _**Agent (composer-2-fast)**_ - Filtering out collections that contain the replacement armature before removing — removing a shared parent collection deleted both characters. @@ -5374,13 +5372,6 @@ _**Agent (composer-2-fast)**_ -_**Agent (composer-2-fast)**_ - - - - - - _**Agent (composer-2-fast)**_ @@ -5434,13 +5425,6 @@ Status: **completed** -_**Agent (composer-2-fast)**_ - - - - - - _**Agent (composer-2-fast)**_ @@ -5475,7 +5459,6 @@ I identified that parent P is safe while grandparent G is unsafe in the recursiv _**Agent (composer-2-fast)**_ - Implementing recursive containment check and filtering the walk-up chain so we never remove a collection whose subtree includes the replacement armature. @@ -5525,13 +5508,6 @@ _**Agent (composer-2-fast)**_ -_**Agent (composer-2-fast)**_ - - - - - - _**Agent (composer-2-fast)**_ @@ -5624,13 +5600,6 @@ Status: **completed** -_**Agent (composer-2-fast)**_ - - - - - - _**Agent (composer-2-fast)**_ @@ -5677,13 +5646,6 @@ Status: **completed** -_**Agent (composer-2-fast)**_ - - - - - - _**Agent (composer-2-fast)**_ @@ -5699,7 +5661,6 @@ _**Agent (composer-2-fast)**_ _**Agent (composer-2-fast)**_ - Here’s what was going wrong and what we changed. **Cause:** `resolve_collection_for_remove_original` could choose a **parent** collection that still held **both** the original and replacement rigs. `bpy.data.collections.remove(coll)` deletes that whole subtree, so the replacement vanished too. @@ -5744,13 +5705,6 @@ looks great. concisely summarize the changes in the past 5 commits in the change --- -_**Agent (composer-2-fast)**_ - - - - - - _**Agent (composer-2-fast)**_ @@ -5796,13 +5750,6 @@ Directory: **T:/Dynamic-Link-Manager** (1 file) -_**Agent (composer-2-fast)**_ - - - - - - _**Agent (composer-2-fast)**_ @@ -5855,7 +5802,6 @@ I'm adding a new release notes section for 0.4.0 summarizing the key changes. +
+Tool use: **mcp-blender-user-blender-get_scene_info** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "get_scene_info", + "parameters": "{\"user_prompt\":\"Inspect scene for RetargRelatives debugging - objects, armatures, parenting\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"{\\n \\\"name\\\": \\\"Scene\\\",\\n \\\"object_count\\\": 346,\\n \\\"objects\\\": [\\n {\\n \\\"name\\\": \\\"Turf\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 3.09,\\n 0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Low Poly Base\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 3.1,\\n 0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Spiral\\\",\\n \\\"type\\\": \\\"CURVE\\\",\\n \\\"location\\\": [\\n 5.99,\\n -0.0,\\n 0.0\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf_Parent\\\",\\n \\\"type\\\": \\\"EMPTY\\\",\\n \\\"location\\\": [\\n 13.8,\\n 26.46,\\n 0.16\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf.001\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 2.84,\\n -0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Low Poly Base.001\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 2.85,\\n 0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Spiral.001\\\",\\n \\\"type\\\": \\\"CURVE\\\",\\n \\\"location\\\": [\\n 5.99,\\n 0.0,\\n 0.0\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf_Parent.001\\\",\\n \\\"type\\\": \\\"EMPTY\\\",\\n \\\"location\\\": [\\n -9.6,\\n 26.46,\\n 0.16\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Strand_1\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n -0.07,\\n -3.61,\\n 0.0\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Strand_2\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n -0.01,\\n -3.61,\\n 0.0\\n ]\\n }\\n ],\\n \\\"materials_count\\\": 99\\n}\"}],\"structuredContent\":{\"result\":\"{\\n \\\"name\\\": \\\"Scene\\\",\\n \\\"object_count\\\": 346,\\n \\\"objects\\\": [\\n {\\n \\\"name\\\": \\\"Turf\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 3.09,\\n 0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Low Poly Base\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 3.1,\\n 0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Spiral\\\",\\n \\\"type\\\": \\\"CURVE\\\",\\n \\\"location\\\": [\\n 5.99,\\n -0.0,\\n 0.0\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf_Parent\\\",\\n \\\"type\\\": \\\"EMPTY\\\",\\n \\\"location\\\": [\\n 13.8,\\n 26.46,\\n 0.16\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf.001\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 2.84,\\n -0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Low Poly Base.001\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n 2.85,\\n 0.0,\\n 0.01\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Spiral.001\\\",\\n \\\"type\\\": \\\"CURVE\\\",\\n \\\"location\\\": [\\n 5.99,\\n 0.0,\\n 0.0\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf_Parent.001\\\",\\n \\\"type\\\": \\\"EMPTY\\\",\\n \\\"location\\\": [\\n -9.6,\\n 26.46,\\n 0.16\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Strand_1\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n -0.07,\\n -3.61,\\n 0.0\\n ]\\n },\\n {\\n \\\"name\\\": \\\"Turf-Strand_2\\\",\\n \\\"type\\\": \\\"MESH\\\",\\n \\\"location\\\": [\\n -0.01,\\n -3.61,\\n 0.0\\n ]\\n }\\n ],\\n \\\"materials_count\\\": 99\\n}\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-get_scene_info** + +
+ +_**Agent (composer-2-fast)**_ + + +
+Tool use: **semantic_search_full** + + +Parameters: + +```json +{ + "explanation": "", + "includePattern": "T:/Dynamic-Link-Manager", + "query": "Where does RetargRelatives retarget objects and what does it skip or require?", + "topK": 10 +} +``` +Result: + +```json +{ + "codeResults": [ + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 524, + "text": "def run_retarg_relatives(orig, rep, rep_descendants, orig_to_rep):" + }, + { + "lineNumber": 525, + "text": " \"\"\"Retarget relations: parents, constraint targets, Armature modifiers to rep. Skip objects in orig's hierarchy (linked collection).\"\"\"" + }, + { + "lineNumber": 526, + "text": " # Replicate orig's parent on rep if it exists (keep world transform)" + }, + { + "lineNumber": 527, + "text": " if orig.parent is not None:" + }, + { + "lineNumber": 528, + "text": " # Store world matrix before reparenting" + }, + { + "lineNumber": 529, + "text": " world_matrix = rep.matrix_world.copy()" + }, + { + "lineNumber": 530, + "text": " rep.parent = orig.parent" + }, + { + "lineNumber": 531, + "text": " rep.parent_type = orig.parent_type" + }, + { + "lineNumber": 532, + "text": " rep.parent_bone = orig.parent_bone" + }, + { + "lineNumber": 533, + "text": " # Restore world matrix" + }, + { + "lineNumber": 534, + "text": " rep.matrix_world = world_matrix" + }, + { + "lineNumber": 535 + }, + { + "lineNumber": 536, + "text": " orig_hierarchy = {orig} | descendants(orig)" + }, + { + "lineNumber": 537, + "text": " candidates = set(rep_descendants)" + }, + { + "lineNumber": 538, + "text": " for ob in bpy.data.objects:" + }, + { + "lineNumber": 539, + "text": " if getattr(ob.parent, \"name\", None) == orig.name:" + }, + { + "lineNumber": 540, + "text": " candidates.add(ob)" + }, + { + "lineNumber": 541, + "text": " for c in getattr(ob, \"constraints\", []):" + }, + { + "lineNumber": 542, + "text": " if getattr(c, \"target\", None) == orig:" + }, + { + "lineNumber": 543, + "text": " candidates.add(ob)" + }, + { + "lineNumber": 544, + "text": " candidates -= orig_hierarchy" + }, + { + "lineNumber": 545, + "text": " for ob in candidates:" + }, + { + "lineNumber": 546, + "text": " if ob.parent == orig:" + }, + { + "lineNumber": 547, + "text": " ob.parent = rep" + }, + { + "lineNumber": 548, + "text": " if ob.type == \"MESH\" and ob.modifiers:" + }, + { + "lineNumber": 549, + "text": " for m in ob.modifiers:" + }, + { + "lineNumber": 550, + "text": " if getattr(m, \"object\", None) == orig:" + }, + { + "lineNumber": 551, + "text": " m.object = rep" + }, + { + "lineNumber": 552 + }, + { + "lineNumber": 553, + "text": " # Retarget constraints on ALL objects (including orig hierarchy like eyes)" + }, + { + "lineNumber": 554, + "text": " for ob in bpy.data.objects:" + }, + { + "lineNumber": 555, + "text": " for c in getattr(ob, \"constraints\", []):" + }, + { + "lineNumber": 556, + "text": " if getattr(c, \"target\", None) == orig:" + }, + { + "lineNumber": 557, + "text": " c.target = rep" + }, + { + "lineNumber": 558 + }, + { + "lineNumber": 559, + "text": " # Retarget bone constraints on ALL armatures (other characters, etc.)" + }, + { + "lineNumber": 560, + "text": " for ob in bpy.data.objects:" + }, + { + "lineNumber": 561, + "text": " if ob.type != \"ARMATURE\" or not ob.pose:" + }, + { + "lineNumber": 562, + "text": " continue" + }, + { + "lineNumber": 563, + "text": " for pbone in ob.pose.bones:" + }, + { + "lineNumber": 564, + "text": " for c in pbone.constraints:" + }, + { + "lineNumber": 565, + "text": " if getattr(c, \"target\", None) == orig:" + }, + { + "lineNumber": 566, + "text": " c.target = rep" + }, + { + "lineNumber": 567 + }, + { + "lineNumber": 568, + "text": " # Camera DOF: retarget focus_object from orig to rep" + } + ], + "range": { + "endPosition": { + "line": 575 + }, + "startPosition": { + "column": 29, + "line": 520 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.40792790055274963 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 524, + "text": "def run_retarg_relatives(orig, rep, rep_descendants, orig_to_rep):" + }, + { + "lineNumber": 566 + }, + { + "lineNumber": 567 + }, + { + "lineNumber": 568, + "text": " # Camera DOF: retarget focus_object from orig to rep" + }, + { + "lineNumber": 569, + "text": " for ob in bpy.data.objects:" + }, + { + "lineNumber": 570, + "text": " if ob.type != 'CAMERA':" + }, + { + "lineNumber": 571, + "text": " continue" + }, + { + "lineNumber": 572, + "text": " camera = ob.data" + }, + { + "lineNumber": 573, + "text": " if not camera.dof:" + }, + { + "lineNumber": 574, + "text": " continue" + }, + { + "lineNumber": 575, + "text": " if camera.dof.focus_object == orig:" + }, + { + "isSignature": true, + "lineNumber": 576, + "text": " camera.dof.focus_object = rep" + }, + { + "lineNumber": 577 + }, + { + "lineNumber": 578 + }, + { + "isSignature": true, + "lineNumber": 579, + "text": "def _base_body_name_match(ob):" + }, + { + "lineNumber": 580, + "text": " \"\"\"True if object looks like the base body mesh (MESH, name has body+base).\"\"\"" + } + ], + "range": { + "endPosition": { + "line": 583 + }, + "startPosition": { + "line": 523 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": { + "ranges": [ + { + "endPosition": { + "column": 5, + "line": 525 + }, + "startPosition": { + "column": 1, + "line": 524 + } + } + ] + } + }, + "score": 0.31106099486351013 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 360, + "text": "class DLM_OT_migrator_retarget_relations(Operator):" + }, + { + "lineNumber": 361, + "text": " bl_idname = \"dlm.migrator_retarget_relations\"" + }, + { + "lineNumber": 362, + "text": " bl_label = \"RetargRelatives\"" + }, + { + "lineNumber": 363, + "text": " bl_description = \"Retarget parent/child and other relations to the replacement character\"" + }, + { + "lineNumber": 364, + "text": " bl_icon = \"ORIENTATION_PARENT\"" + }, + { + "lineNumber": 365, + "text": " bl_options = {\"REGISTER\", \"UNDO\"}" + }, + { + "lineNumber": 366 + }, + { + "lineNumber": 367, + "text": " def execute(self, context):" + }, + { + "lineNumber": 368, + "text": " orig, rep = _get_migrator_pair(context)" + }, + { + "lineNumber": 369, + "text": " if not orig or not rep or orig == rep:" + }, + { + "lineNumber": 370, + "text": " self.report({\"ERROR\"}, \"No valid character pair.\")" + }, + { + "lineNumber": 371, + "text": " return {\"CANCELLED\"}" + }, + { + "lineNumber": 372, + "text": " try:" + }, + { + "lineNumber": 373, + "text": " from ..ops.migrator import run_retarg_relatives" + }, + { + "lineNumber": 374, + "text": " from ..utils import descendants" + }, + { + "lineNumber": 375, + "text": " rep_descendants = descendants(rep)" + }, + { + "lineNumber": 376, + "text": " orig_to_rep = {orig: rep}" + }, + { + "lineNumber": 377, + "text": " run_retarg_relatives(orig, rep, rep_descendants, orig_to_rep)" + }, + { + "lineNumber": 378, + "text": " self.report({\"INFO\"}, \"Retarget relations done.\")" + }, + { + "lineNumber": 379, + "text": " return {\"FINISHED\"}" + }, + { + "lineNumber": 380, + "text": " except Exception as e:" + }, + { + "lineNumber": 381, + "text": " self.report({\"ERROR\"}, str(e))" + }, + { + "isSignature": true, + "lineNumber": 382, + "text": " return {\"CANCELLED\"}" + }, + { + "lineNumber": 383 + }, + { + "lineNumber": 384 + }, + { + "isSignature": true, + "lineNumber": 385, + "text": "class DLM_OT_migrator_basebody_shapekeys(Operator):" + }, + { + "lineNumber": 386, + "text": " bl_idname = \"dlm.migrator_basebody_shapekeys\"" + } + ], + "range": { + "endPosition": { + "line": 414 + }, + "startPosition": { + "column": 32, + "line": 356 + } + }, + "relativeWorkspacePath": ".\\ui\\operators.py", + "signatures": {} + }, + "score": 0.2795126438140869 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 768, + "text": "def run_full_migration(context):" + }, + { + "lineNumber": 769, + "text": " \"\"\"" + }, + { + "lineNumber": 770, + "text": " Run the full 7-step character migration for the single pair (manual or automatic)." + }, + { + "lineNumber": 771, + "text": " Returns (True, message) on success, (False, error_message) on failure." + }, + { + "lineNumber": 772, + "text": " \"\"\"" + }, + { + "lineNumber": 773, + "text": " props = getattr(context.scene, \"dynamic_link_manager\", None)" + }, + { + "lineNumber": 774, + "text": " use_auto = props and getattr(props, \"migrator_mode\", False)" + }, + { + "lineNumber": 775, + "text": " orig, rep = (get_pair_automatic(context) if use_auto else get_pair_manual(context))" + }, + { + "lineNumber": 776, + "text": " if not orig or not rep:" + }, + { + "lineNumber": 777, + "text": " return False, \"No character pair (set Original/Replacement or enable Automatic).\"" + }, + { + "lineNumber": 778, + "text": " if orig == rep:" + }, + { + "lineNumber": 779, + "text": " return False, \"Original and replacement must be different armatures.\"" + }, + { + "lineNumber": 780 + }, + { + "lineNumber": 781, + "text": " orig_to_rep = {orig: rep}" + }, + { + "lineNumber": 782, + "text": " rep_descendants = descendants(rep)" + }, + { + "lineNumber": 783 + }, + { + "lineNumber": 784, + "text": " try:" + }, + { + "lineNumber": 785, + "text": " run_copy_attr(orig, rep)" + }, + { + "lineNumber": 786, + "text": " run_mig_nla(orig, rep, context=context)" + }, + { + "lineNumber": 787, + "text": " run_mig_cust_props(orig, rep)" + }, + { + "lineNumber": 788, + "text": " run_mig_bone_const(orig, rep, orig_to_rep)" + }, + { + "lineNumber": 789, + "text": " run_retarg_relatives(orig, rep, rep_descendants, orig_to_rep)" + }, + { + "lineNumber": 790, + "text": " run_mig_bbody_shapekeys(orig, rep, rep_descendants, context)" + }, + { + "lineNumber": 791, + "text": " except Exception as e:" + }, + { + "lineNumber": 792, + "text": " return False, str(e)" + }, + { + "isSignature": true, + "lineNumber": 793, + "text": " return True, f\"Migrated {orig.name} → {rep.name}\"" + } + ], + "range": { + "endPosition": { + "column": 55, + "line": 792 + }, + "startPosition": { + "column": 45, + "line": 764 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.2621721923351288 + }, + { + "codeBlock": { + "detailedLines": [ + { + "lineNumber": 1, + "text": "# Changelog" + }, + { + "lineNumber": 2 + }, + { + "lineNumber": 3, + "text": "## [0.4.0] - 2026-03-25" + }, + { + "lineNumber": 4 + }, + { + "lineNumber": 5, + "text": "### Added" + }, + { + "lineNumber": 6, + "text": "- CharMig: ARP (Auto-Rig Pro) support alongside Rigify (rig-family selector and related migrator behavior)." + }, + { + "lineNumber": 7, + "text": "- MigBBody shapekeys: include manually specified meshes in migration, not only auto-detected base body." + }, + { + "lineNumber": 8 + }, + { + "lineNumber": 9, + "text": "### Changed" + }, + { + "lineNumber": 10, + "text": "- MigNLA: activate the replacement character before turning on Animation Layers, so AnimLayers apply without manually selecting rep first." + }, + { + "lineNumber": 11 + }, + { + "lineNumber": 12, + "text": "### Fixed" + }, + { + "lineNumber": 13, + "text": "- Remove Original: when resolving which collection to delete, never remove a collection whose subtree still contains the replacement armature (avoids deleting both orig and rep when they share hierarchy); Blender 5 collection parent checks use collection names." + }, + { + "lineNumber": 14 + }, + { + "lineNumber": 15, + "text": "## [0.3.0] - 2026-03-13" + }, + { + "lineNumber": 16 + }, + { + "lineNumber": 17, + "text": "### Added" + }, + { + "lineNumber": 18, + "text": "- Camera DOF focus_object retargeting in RetargRelations." + }, + { + "lineNumber": 19, + "text": "- Bone constraint retargeting for ALL armatures (other characters, props)." + }, + { + "lineNumber": 20, + "text": "- New \"Body\" tweak mode: spine/torso tweaks without arm/leg." + }, + { + "lineNumber": 21, + "text": "- \"Add All\" tweaks now includes body, spine, and MCH tweaks." + }, + { + "lineNumber": 22 + }, + { + "lineNumber": 23, + "text": "### Changed" + }, + { + "lineNumber": 24, + "text": "- Constraint retargeting now works on all objects (including orig hierarchy eyes)." + }, + { + "lineNumber": 25, + "text": "- Tweak UI: renamed \"Both\" to \"All\", added \"Body\" row." + }, + { + "lineNumber": 26 + }, + { + "lineNumber": 27, + "text": "### Fixed" + }, + { + "lineNumber": 28, + "text": "- Blender 5.0 bone selection API compatibility (pose_bone.select)." + }, + { + "lineNumber": 29 + }, + { + "lineNumber": 30, + "text": "## [0.2.0] - 2026-03-10" + }, + { + "lineNumber": 31 + }, + { + "lineNumber": 32, + "text": "### Added" + }, + { + "lineNumber": 33, + "text": "- New \"Remove Original\" operator with collection deletion and action management (purge/cleanup)." + }, + { + "lineNumber": 34, + "text": "- FK rotation migration improvements: baking support, single-frame copy, armature parent replication in RetargRelatives." + }, + { + "lineNumber": 35 + }, + { + "lineNumber": 36, + "text": "### Changed" + }, + { + "lineNumber": 37, + "text": "- UI improvements: Tweak Tools section is now collapsible; reorganized into Situational Fixes section." + }, + { + "lineNumber": 38, + "text": "- Removed \"Run Migration\" button (functionality merged into Remove Original workflow)." + }, + { + "lineNumber": 39 + }, + { + "lineNumber": 40, + "text": "### Fixed" + }, + { + "lineNumber": 41, + "text": "- MigFKRot: fixed error printing and improved bone matching reliability." + }, + { + "lineNumber": 42 + }, + { + "lineNumber": 43, + "text": "## [0.1.2] - 2026-02-19" + }, + { + "lineNumber": 44 + }, + { + "lineNumber": 45, + "text": "### Added" + }, + { + "lineNumber": 46, + "text": "- MigFKRot operator: copy FK arm and finger rotations from original to replacement using pose matrix copy." + }, + { + "lineNumber": 47 + }, + { + "lineNumber": 48, + "text": "### Changed" + }, + { + "lineNumber": 49, + "text": "- MigNLA: duplicate actions when copying to repchar so editing on repchar doesn't affect origchar." + }, + { + "lineNumber": 50, + "text": "- MigFKRot: expanded bone name pattern matching for various rig styles (Rigify and alternatives)." + } + ], + "range": { + "endPosition": { + "line": 50 + }, + "startPosition": {} + }, + "relativeWorkspacePath": ".\\CHANGELOG.md", + "signatures": {} + }, + "score": 0.2068084478378296 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 112, + "text": "def resolve_collection_for_remove_original(orig, rig_family, scene, rep=None):" + }, + { + "lineNumber": 113, + "text": " \"\"\"" + }, + { + "lineNumber": 114, + "text": " Return a collection to remove for Remove Original, or None to fall back to object-only removal." + }, + { + "lineNumber": 115 + }, + { + "lineNumber": 116, + "text": " Walks up from the deepest users_collection so nested linked rigs remove the outer instance," + }, + { + "lineNumber": 117, + "text": " not an inner linked child collection." + }, + { + "lineNumber": 118 + }, + { + "lineNumber": 119, + "text": " If rep is the replacement armature, never remove a collection whose subtree contains rep" + }, + { + "lineNumber": 120, + "text": " (avoids deleting both characters when they share a parent collection)." + }, + { + "lineNumber": 121 + }, + { + "lineNumber": 122, + "text": " rig_family: 'RIGIFY' | 'ARP' (ARP skips Rigify name heuristics in _pick_remove_target_from_chain)." + }, + { + "lineNumber": 123, + "text": " \"\"\"" + }, + { + "lineNumber": 124, + "text": " if not orig or orig.type != \"ARMATURE\" or orig.name not in bpy.data.objects:" + }, + { + "lineNumber": 125, + "text": " return None" + }, + { + "lineNumber": 126 + }, + { + "lineNumber": 127, + "text": " inner = _deepest_users_collection(scene, orig)" + }, + { + "lineNumber": 128, + "text": " if inner is None:" + }, + { + "lineNumber": 129, + "text": " return None" + }, + { + "lineNumber": 130 + }, + { + "lineNumber": 131, + "text": " chain = _walk_up_chain(scene, inner)" + }, + { + "lineNumber": 132, + "text": " if not chain:" + }, + { + "lineNumber": 133, + "text": " return None" + }, + { + "lineNumber": 134 + }, + { + "lineNumber": 135, + "text": " if rep is not None and rep.name in bpy.data.objects:" + }, + { + "lineNumber": 136, + "text": " chain = [c for c in chain if not _collection_contains_object_recursive(c, rep)]" + }, + { + "lineNumber": 137, + "text": " if not chain:" + }, + { + "lineNumber": 138, + "text": " return None" + }, + { + "lineNumber": 139 + }, + { + "isSignature": true, + "lineNumber": 140, + "text": " return _pick_remove_target_from_chain(orig, chain, rig_family)" + } + ], + "range": { + "endPosition": { + "column": 66, + "line": 139 + }, + "startPosition": { + "column": 20, + "line": 108 + } + }, + "relativeWorkspacePath": ".\\utils\\remove_original.py", + "signatures": {} + }, + "score": 0.2036132961511612 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 453, + "text": "def _copy_constraint_props(c, nc, orig, rep, orig_to_rep):" + }, + { + "lineNumber": 454, + "text": " \"\"\"Copy all copyable RNA properties from c to nc, retargeting object/armature pointers.\"\"\"" + }, + { + "lineNumber": 455, + "text": " for rna_prop in c.bl_rna.properties:" + }, + { + "lineNumber": 456, + "text": " if rna_prop.is_readonly or rna_prop.identifier in (\"name\", \"type\"):" + }, + { + "lineNumber": 457, + "text": " continue" + }, + { + "lineNumber": 458, + "text": " if not hasattr(nc, rna_prop.identifier):" + }, + { + "lineNumber": 459, + "text": " continue" + }, + { + "lineNumber": 460, + "text": " try:" + }, + { + "lineNumber": 461, + "text": " val = getattr(c, rna_prop.identifier)" + }, + { + "lineNumber": 462, + "text": " except Exception:" + }, + { + "lineNumber": 463, + "text": " continue" + }, + { + "lineNumber": 464, + "text": " rna_type = getattr(rna_prop, \"type\", None)" + }, + { + "lineNumber": 465, + "text": " if rna_type == \"POINTER\":" + }, + { + "lineNumber": 466, + "text": " setattr(nc, rna_prop.identifier, _retarget_id(val, orig, rep, orig_to_rep))" + }, + { + "lineNumber": 467, + "text": " elif rna_type == \"COLLECTION\":" + }, + { + "lineNumber": 468, + "text": " # e.g. ArmatureConstraint.targets: ensure count then copy item props (target, subtarget, weight)" + }, + { + "lineNumber": 469, + "text": " try:" + }, + { + "lineNumber": 470, + "text": " dst_coll = getattr(nc, rna_prop.identifier)" + }, + { + "lineNumber": 471, + "text": " src_coll = getattr(c, rna_prop.identifier)" + }, + { + "lineNumber": 472, + "text": " add_fn = getattr(dst_coll, \"add\", None) or getattr(dst_coll, \"new\", None)" + }, + { + "lineNumber": 473, + "text": " for i in range(len(src_coll)):" + }, + { + "lineNumber": 474, + "text": " if i \u003e= len(dst_coll) and add_fn:" + }, + { + "lineNumber": 475, + "text": " add_fn()" + }, + { + "lineNumber": 476, + "text": " for i, src_item in enumerate(src_coll):" + }, + { + "lineNumber": 477, + "text": " if i \u003e= len(dst_coll):" + }, + { + "lineNumber": 478, + "text": " break" + }, + { + "lineNumber": 479, + "text": " dst_item = dst_coll[i]" + }, + { + "lineNumber": 480, + "text": " for p in dst_item.bl_rna.properties:" + }, + { + "lineNumber": 481, + "text": " if p.is_readonly or p.identifier == \"name\":" + }, + { + "lineNumber": 482, + "text": " continue" + }, + { + "lineNumber": 483, + "text": " if not hasattr(dst_item, p.identifier):" + }, + { + "lineNumber": 484, + "text": " continue" + }, + { + "lineNumber": 485, + "text": " try:" + }, + { + "lineNumber": 486, + "text": " v = getattr(src_item, p.identifier)" + }, + { + "lineNumber": 487, + "text": " if getattr(p, \"type\", None) == \"POINTER\":" + }, + { + "lineNumber": 488, + "text": " v = _retarget_id(v, orig, rep, orig_to_rep)" + }, + { + "lineNumber": 489, + "text": " setattr(dst_item, p.identifier, v)" + }, + { + "lineNumber": 490, + "text": " except Exception:" + }, + { + "lineNumber": 491, + "text": " pass" + }, + { + "lineNumber": 492, + "text": " except Exception:" + }, + { + "lineNumber": 493, + "text": " pass" + }, + { + "lineNumber": 494, + "text": " else:" + }, + { + "lineNumber": 495, + "text": " try:" + }, + { + "lineNumber": 496, + "text": " setattr(nc, rna_prop.identifier, val)" + } + ], + "range": { + "endPosition": { + "line": 497 + }, + "startPosition": { + "column": 34, + "line": 449 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.19048446416854858 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 740, + "text": "def run_mig_bbody_shapekeys(orig, rep, rep_descendants, context=None):" + }, + { + "lineNumber": 741, + "text": " \"\"\"Replacement base body: library override (fully editable when context given), copy shapekey values, then shape-key action.\"\"\"" + }, + { + "lineNumber": 742, + "text": " props = getattr(context.scene, \"dynamic_link_manager\", None) if context else None" + }, + { + "lineNumber": 743, + "text": " if props and getattr(props, \"migbbody_manual_override\", False):" + }, + { + "lineNumber": 744, + "text": " mo = getattr(props, \"migbbody_orig_body\", None)" + }, + { + "lineNumber": 745, + "text": " mr = getattr(props, \"migbbody_rep_body\", None)" + }, + { + "lineNumber": 746, + "text": " if mo and mr and mo.type == \"MESH\" and mr.type == \"MESH\":" + }, + { + "lineNumber": 747, + "text": " _process_mig_bbody_mesh(mo, mr, context)" + }, + { + "lineNumber": 748, + "text": " return" + }, + { + "lineNumber": 749 + }, + { + "lineNumber": 750, + "text": " orig_descendants = list(descendants(orig))" + }, + { + "lineNumber": 751, + "text": " any_auto = False" + }, + { + "lineNumber": 752, + "text": " for ob in list(rep_descendants):" + }, + { + "lineNumber": 753, + "text": " if not _base_body_name_match(ob):" + }, + { + "lineNumber": 754, + "text": " continue" + }, + { + "lineNumber": 755, + "text": " if ob.modifiers:" + }, + { + "lineNumber": 756, + "text": " for m in ob.modifiers:" + }, + { + "lineNumber": 757, + "text": " if m.type == \"ARMATURE\" and m.object == rep:" + }, + { + "lineNumber": 758, + "text": " break" + }, + { + "lineNumber": 759, + "text": " else:" + }, + { + "lineNumber": 760, + "text": " continue" + }, + { + "lineNumber": 761, + "text": " any_auto = True" + }, + { + "lineNumber": 762, + "text": " orig_base = _find_base_body(orig, orig_descendants, rep_base_name=ob.name)" + }, + { + "lineNumber": 763, + "text": " _process_mig_bbody_mesh(orig_base, ob, context)" + }, + { + "lineNumber": 764, + "text": " if not any_auto and props is not None:" + }, + { + "isSignature": true, + "lineNumber": 765, + "text": " props.migbbody_manual_override = True" + }, + { + "lineNumber": 766 + }, + { + "lineNumber": 767 + }, + { + "isSignature": true, + "lineNumber": 768, + "text": "def run_full_migration(context):" + }, + { + "lineNumber": 769, + "text": " \"\"\"" + } + ], + "range": { + "endPosition": { + "line": 792 + }, + "startPosition": { + "column": 28, + "line": 736 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.18986093997955322 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 56, + "text": "class DLM_PT_main_panel(Panel):" + }, + { + "lineNumber": 66, + "text": "def draw(self, context):" + }, + { + "lineNumber": 95, + "text": " icon=\"CONSTRAINT_BONE\")" + }, + { + "lineNumber": 96, + "text": " row.operator(\"dlm.migrator_retarget_relations\", text=\"RetargRelatives\", icon=\"ORIENTATION_PARENT\")" + }, + { + "lineNumber": 97 + }, + { + "lineNumber": 98, + "text": " # Situational" + }, + { + "lineNumber": 99, + "text": " situational_box = layout.box()" + }, + { + "lineNumber": 100, + "text": " situational_box.label(text=\"Situational Fixes\", icon=\"QUESTION\")" + }, + { + "lineNumber": 101, + "text": " row = situational_box.row()" + }, + { + "lineNumber": 102, + "text": " row.prop(props, \"migbbody_manual_override\", text=\"Manual body meshes\")" + }, + { + "lineNumber": 103, + "text": " row = situational_box.row()" + }, + { + "lineNumber": 104, + "text": " row.enabled = props.migbbody_manual_override" + }, + { + "lineNumber": 105, + "text": " row.prop(props, \"migbbody_orig_body\", text=\"Original body\")" + }, + { + "lineNumber": 106, + "text": " row = situational_box.row()" + }, + { + "lineNumber": 107, + "text": " row.enabled = props.migbbody_manual_override" + }, + { + "lineNumber": 108, + "text": " row.prop(props, \"migbbody_rep_body\", text=\"Replacement body\")" + }, + { + "lineNumber": 109, + "text": " row = situational_box.row(align=True)" + }, + { + "lineNumber": 110, + "text": " row.operator(\"dlm.migrator_basebody_shapekeys\", text=\"MigBBodyShapeKeys\", icon=\"SHAPEKEY_DATA\")" + }, + { + "lineNumber": 111, + "text": " row = situational_box.row(align=True)" + }, + { + "lineNumber": 112, + "text": " row.operator(\"dlm.migrator_fk_rotations\", text=\"MigFKRot\", icon=\"BONE_DATA\")" + }, + { + "lineNumber": 113, + "text": " row.operator(\"dlm.migrator_fk_rotations_remove\", text=\"Remove\", icon=\"X\")" + }, + { + "lineNumber": 114, + "text": " row.operator(\"dlm.migrator_fk_rotations_bake\", text=\"Bake\", icon=\"KEYFRAME\")" + }, + { + "lineNumber": 115 + }, + { + "lineNumber": 116, + "text": " # Tweak Tools: header row (always), main box only when expanded" + }, + { + "lineNumber": 117, + "text": " section_icon = \"DISCLOSURE_TRI_DOWN\" if props.tweak_tools_section_expanded else \"DISCLOSURE_TRI_RIGHT\"" + }, + { + "lineNumber": 118, + "text": " row = layout.row(align=True)" + }, + { + "lineNumber": 119, + "text": " row.prop(props, \"tweak_tools_section_expanded\", text=\"\", icon=section_icon, icon_only=True)" + }, + { + "lineNumber": 120, + "text": " row.label(text=\"Tweak Tools\", icon=\"CONSTRAINT\")" + }, + { + "lineNumber": 121, + "text": " if props.tweak_tools_section_expanded:" + }, + { + "lineNumber": 122, + "text": " tweak_box = layout.box()" + }, + { + "lineNumber": 123, + "text": " row = tweak_box.row(align=True)" + } + ], + "range": { + "endPosition": { + "line": 190 + }, + "startPosition": { + "line": 55 + } + }, + "relativeWorkspacePath": ".\\ui\\panels.py", + "signatures": { + "ranges": [ + { + "endPosition": { + "column": 5, + "line": 57 + }, + "startPosition": { + "column": 1, + "line": 56 + } + }, + { + "endPosition": { + "column": 9, + "line": 67 + }, + "startPosition": { + "column": 5, + "line": 66 + } + } + ] + } + }, + "score": 0.1856141984462738 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 199, + "text": "def bake_fk_rotations(context, orig, rep, track_name=None, post_clean=False):" + }, + { + "lineNumber": 200, + "text": " \"\"\"" + }, + { + "lineNumber": 201, + "text": " Bake FK arm/finger rotations to a new NLA track with replace mode." + }, + { + "lineNumber": 202, + "text": " Returns (True, message) or (False, error_message)." + }, + { + "lineNumber": 203, + "text": " \"\"\"" + }, + { + "lineNumber": 204, + "text": " print(f\"[DLM MigFKRot Bake] START: orig={orig.name}, rep={rep.name}\")" + }, + { + "lineNumber": 205, + "text": " " + }, + { + "lineNumber": 206, + "text": " fk_names = _get_fk_bones(rep)" + }, + { + "lineNumber": 207, + "text": " print(f\"[DLM MigFKRot Bake] Found {len(fk_names)} FK bones: {fk_names[:5]}...\")" + }, + { + "lineNumber": 208, + "text": " " + }, + { + "lineNumber": 209, + "text": " if not fk_names:" + }, + { + "lineNumber": 210, + "text": " return False, f\"No FK bones found on {rep.name}\"" + }, + { + "lineNumber": 211 + }, + { + "lineNumber": 212, + "text": " # Filter to bones that exist on both" + }, + { + "lineNumber": 213, + "text": " common_bones = [n for n in fk_names if n in orig.pose.bones and n in rep.pose.bones]" + }, + { + "lineNumber": 214, + "text": " print(f\"[DLM MigFKRot Bake] {len(common_bones)} common bones between orig and rep\")" + }, + { + "lineNumber": 215, + "text": " " + }, + { + "lineNumber": 216, + "text": " if not common_bones:" + }, + { + "lineNumber": 217, + "text": " return False, \"No matching FK bones found on both armatures\"" + }, + { + "lineNumber": 218 + }, + { + "lineNumber": 219, + "text": " # Get source action for frame range (from keyframes, not strip bounds)" + }, + { + "lineNumber": 220, + "text": " source_action = None" + }, + { + "lineNumber": 221, + "text": " if rep.animation_data:" + }, + { + "lineNumber": 222, + "text": " if rep.animation_data.action:" + }, + { + "lineNumber": 223, + "text": " source_action = rep.animation_data.action" + }, + { + "lineNumber": 224, + "text": " elif rep.animation_data.nla_tracks:" + }, + { + "lineNumber": 225, + "text": " for track in rep.animation_data.nla_tracks:" + }, + { + "lineNumber": 226, + "text": " if track.strips:" + }, + { + "lineNumber": 227, + "text": " for strip in track.strips:" + }, + { + "lineNumber": 228, + "text": " if strip.action:" + }, + { + "lineNumber": 229, + "text": " source_action = strip.action" + }, + { + "lineNumber": 230, + "text": " break" + }, + { + "lineNumber": 231, + "text": " if source_action:" + }, + { + "lineNumber": 232, + "text": " break" + }, + { + "lineNumber": 233, + "text": " " + }, + { + "lineNumber": 234, + "text": " # Get frame range from source action keyframes" + }, + { + "lineNumber": 235, + "text": " frame_range = _get_action_frame_range(source_action) if source_action else None" + }, + { + "lineNumber": 236, + "text": " " + }, + { + "lineNumber": 237, + "text": " if not frame_range:" + }, + { + "lineNumber": 238, + "text": " # Fallback to scene range" + }, + { + "lineNumber": 239, + "text": " frame_range = (context.scene.frame_start, context.scene.frame_end)" + }, + { + "lineNumber": 240, + "text": " " + }, + { + "lineNumber": 241, + "text": " frame_start, frame_end = frame_range" + }, + { + "lineNumber": 242 + }, + { + "lineNumber": 243, + "text": " # Ensure rep is active and in pose mode" + }, + { + "lineNumber": 244, + "text": " if context.view_layer.objects.active != rep:" + }, + { + "lineNumber": 245, + "text": " context.view_layer.objects.active = rep" + }, + { + "lineNumber": 246, + "text": " if rep.mode != \"POSE\":" + }, + { + "lineNumber": 247, + "text": " bpy.ops.object.mode_set(mode=\"POSE\")" + }, + { + "lineNumber": 248 + }, + { + "lineNumber": 249, + "text": " # Step 1: Check for existing COPY_TRANSFORMS constraints" + } + ], + "range": { + "endPosition": { + "line": 376 + }, + "startPosition": { + "column": 15, + "line": 195 + } + }, + "relativeWorkspacePath": ".\\ops\\fk_rotations.py", + "signatures": {} + }, + "score": 0.18171286582946777 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 92, + "text": "def bake_tweak_constraints(context, orig, rep, limb, track_name, post_clean):" + }, + { + "lineNumber": 93, + "text": " \"\"\"" + }, + { + "lineNumber": 94, + "text": " Select rep's tweak bones, run nla.bake, optionally run clean + decimate." + }, + { + "lineNumber": 95, + "text": " Returns (True, message) or (False, error_message)." + }, + { + "lineNumber": 96, + "text": " \"\"\"" + }, + { + "lineNumber": 97, + "text": " names = get_tweak_bones(rep, limb)" + }, + { + "lineNumber": 98, + "text": " if not names:" + }, + { + "lineNumber": 99, + "text": " return False, f\"No tweak bones found for {limb} on {rep.name}\"" + }, + { + "lineNumber": 100 + }, + { + "lineNumber": 101, + "text": " scene = context.scene" + }, + { + "lineNumber": 102, + "text": " frame_range = _frame_range_from_track(rep, track_name) if track_name else None" + }, + { + "lineNumber": 103, + "text": " if not frame_range:" + }, + { + "lineNumber": 104, + "text": " frame_range = (scene.frame_start, scene.frame_end)" + }, + { + "lineNumber": 105, + "text": " frame_start, frame_end = frame_range" + }, + { + "lineNumber": 106 + }, + { + "lineNumber": 107, + "text": " # Ensure rep is active and in pose mode" + }, + { + "lineNumber": 108, + "text": " if context.view_layer.objects.active != rep:" + }, + { + "lineNumber": 109, + "text": " context.view_layer.objects.active = rep" + }, + { + "lineNumber": 110, + "text": " if rep.mode != \"POSE\":" + }, + { + "lineNumber": 111, + "text": " bpy.ops.object.mode_set(mode=\"POSE\")" + }, + { + "lineNumber": 112 + }, + { + "lineNumber": 113, + "text": " # Select only tweak bones on rep" + }, + { + "lineNumber": 114, + "text": " bpy.ops.pose.select_all(action=\"DESELECT\")" + }, + { + "lineNumber": 115, + "text": " for name in names:" + }, + { + "lineNumber": 116, + "text": " rep.pose.bones[name].select = True" + }, + { + "lineNumber": 117 + }, + { + "lineNumber": 118, + "text": " # Bake" + }, + { + "lineNumber": 119, + "text": " try:" + }, + { + "lineNumber": 120, + "text": " bpy.ops.nla.bake(" + }, + { + "lineNumber": 121, + "text": " frame_start=frame_start," + }, + { + "lineNumber": 122, + "text": " frame_end=frame_end," + }, + { + "lineNumber": 123, + "text": " step=1," + }, + { + "lineNumber": 124, + "text": " only_selected=True," + }, + { + "lineNumber": 125, + "text": " visual_keying=True," + }, + { + "lineNumber": 126, + "text": " clear_constraints=True," + }, + { + "lineNumber": 127, + "text": " clear_parents=True," + }, + { + "lineNumber": 128, + "text": " use_current_action=True," + }, + { + "lineNumber": 129, + "text": " clean_curves=False," + }, + { + "lineNumber": 130, + "text": " bake_types={\"POSE\"}," + }, + { + "lineNumber": 131, + "text": " channel_types={\"LOCATION\", \"ROTATION\"}," + }, + { + "lineNumber": 132, + "text": " )" + }, + { + "lineNumber": 133, + "text": " except Exception as e:" + }, + { + "lineNumber": 134, + "text": " return False, str(e)" + }, + { + "lineNumber": 135 + }, + { + "lineNumber": 136, + "text": " if not post_clean:" + }, + { + "lineNumber": 137, + "text": " return True, f\"Baked {len(names)} tweak bones ({frame_start}-{frame_end}).\"" + }, + { + "lineNumber": 138 + }, + { + "lineNumber": 139, + "text": " # Post-clean: find an area we can use for action/graph ops" + }, + { + "lineNumber": 140, + "text": " win = context.window" + }, + { + "lineNumber": 141, + "text": " for area in win.screen.areas:" + }, + { + "lineNumber": 142, + "text": " if area.type == \"DOPESHEET_EDITOR\":" + }, + { + "lineNumber": 143, + "text": " with context.temp_override(window=win, area=area):" + }, + { + "lineNumber": 144, + "text": " try:" + }, + { + "lineNumber": 145, + "text": " bpy.ops.action.clean_keyframes()" + }, + { + "lineNumber": 146, + "text": " except Exception:" + }, + { + "lineNumber": 147, + "text": " pass" + }, + { + "lineNumber": 148, + "text": " break" + }, + { + "lineNumber": 149, + "text": " for area in win.screen.areas:" + }, + { + "lineNumber": 150, + "text": " if area.type == \"GRAPH_EDITOR\":" + } + ], + "range": { + "endPosition": { + "line": 157 + }, + "startPosition": { + "column": 33, + "line": 88 + } + }, + "relativeWorkspacePath": ".\\ops\\tweak_tools.py", + "signatures": {} + }, + "score": 0.17914366722106934 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 174, + "text": "def attempt_relink(context, report, addon_name=None):" + }, + { + "lineNumber": 175, + "text": " if addon_name is None:" + }, + { + "lineNumber": 176, + "text": " addon_name = __package__.rsplit(\".\", 1)[0] if \".\" in __package__ else __package__" + }, + { + "lineNumber": 177, + "text": " prefs = context.preferences.addons.get(addon_name)" + }, + { + "lineNumber": 178, + "text": " if not prefs or not prefs.preferences.search_paths:" + }, + { + "lineNumber": 179, + "text": " report({\"ERROR\"}, \"No search paths configured.\")" + }, + { + "lineNumber": 180, + "text": " return {\"CANCELLED\"}" + }, + { + "lineNumber": 181, + "text": " missing_libs = [lib for lib in context.scene.dynamic_link_manager.linked_libraries if lib.is_missing]" + }, + { + "lineNumber": 182, + "text": " if not missing_libs:" + }, + { + "lineNumber": 183, + "text": " report({\"INFO\"}, \"No missing libraries to relink\")" + }, + { + "lineNumber": 184, + "text": " return {\"FINISHED\"}" + }, + { + "lineNumber": 185, + "text": " report({\"INFO\"}, f\"Attempting to relink {len(missing_libs)} missing libraries...\")" + }, + { + "lineNumber": 186, + "text": " files_dir_list = []" + }, + { + "lineNumber": 187, + "text": " try:" + }, + { + "lineNumber": 188, + "text": " for search_path in prefs.preferences.search_paths:" + }, + { + "lineNumber": 189, + "text": " if search_path.path:" + }, + { + "lineNumber": 190, + "text": " for dirpath, dirnames, filenames in os.walk(bpy.path.abspath(search_path.path)):" + }, + { + "lineNumber": 191, + "text": " files_dir_list.append([dirpath, filenames])" + }, + { + "lineNumber": 192, + "text": " except FileNotFoundError:" + }, + { + "lineNumber": 193, + "text": " report({\"ERROR\"}, \"Bad file path in search paths\")" + }, + { + "lineNumber": 194, + "text": " return {\"CANCELLED\"}" + }, + { + "lineNumber": 195, + "text": " relinked_count = 0" + }, + { + "lineNumber": 196, + "text": " for lib_item in missing_libs:" + }, + { + "lineNumber": 197, + "text": " lib_filename = os.path.basename(lib_item.filepath)" + }, + { + "lineNumber": 198, + "text": " for dirpath, filenames in files_dir_list:" + }, + { + "lineNumber": 199, + "text": " if lib_filename in filenames:" + }, + { + "lineNumber": 200, + "text": " try:" + }, + { + "lineNumber": 201, + "text": " bpy.ops.file.find_missing_files()" + }, + { + "lineNumber": 202, + "text": " relinked_count += 1" + }, + { + "lineNumber": 203, + "text": " except Exception:" + }, + { + "lineNumber": 204, + "text": " pass" + }, + { + "lineNumber": 205, + "text": " break" + }, + { + "lineNumber": 206, + "text": " report({\"INFO\"}, f\"Relink attempt complete. Relinked: {relinked_count}\")" + }, + { + "isSignature": true, + "lineNumber": 207, + "text": " return {\"FINISHED\"}" + } + ], + "range": { + "endPosition": { + "column": 23, + "line": 206 + }, + "startPosition": { + "column": 23, + "line": 170 + } + }, + "relativeWorkspacePath": ".\\ops\\library.py", + "signatures": {} + }, + "score": 0.17477740347385406 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 444, + "text": "def _retarget_id(ob, orig, rep, orig_to_rep):" + }, + { + "lineNumber": 445, + "text": " \"\"\"Return rep, orig_to_rep[ob], or ob so constraint targets point to replacement when appropriate.\"\"\"" + }, + { + "lineNumber": 446, + "text": " if ob is None:" + }, + { + "lineNumber": 447, + "text": " return None" + }, + { + "lineNumber": 448, + "text": " if ob == orig:" + }, + { + "lineNumber": 449, + "text": " return rep" + }, + { + "isSignature": true, + "lineNumber": 450, + "text": " return orig_to_rep.get(ob, ob)" + }, + { + "lineNumber": 451 + }, + { + "lineNumber": 452 + }, + { + "isSignature": true, + "lineNumber": 453, + "text": "def _copy_constraint_props(c, nc, orig, rep, orig_to_rep):" + }, + { + "lineNumber": 454, + "text": " \"\"\"" + } + ], + "range": { + "endPosition": { + "line": 497 + }, + "startPosition": { + "column": 74, + "line": 440 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.17353519797325134 + }, + { + "codeBlock": { + "detailedLines": [ + { + "lineNumber": 48, + "text": "### Changed" + }, + { + "lineNumber": 49, + "text": "- MigNLA: duplicate actions when copying to repchar so editing on repchar doesn't affect origchar." + }, + { + "lineNumber": 50, + "text": "- MigFKRot: expanded bone name pattern matching for various rig styles (Rigify and alternatives)." + }, + { + "lineNumber": 51, + "text": "- MigBBody shapekeys: also duplicate shape key actions for independence." + }, + { + "lineNumber": 52 + }, + { + "lineNumber": 53, + "text": "### Fixed" + }, + { + "lineNumber": 54, + "text": "- MigFKRot: add debug logging to show which bones are found and copied." + }, + { + "lineNumber": 55 + }, + { + "lineNumber": 56 + }, + { + "lineNumber": 57, + "text": "## [0.1.1] - 2026-02-19" + }, + { + "lineNumber": 58 + }, + { + "lineNumber": 59, + "text": "### Fixed" + }, + { + "lineNumber": 60, + "text": "- MigBBody shapekeys: action slot not applying; now copy action and slot props (last_slot_identifier, action_slot, blend/extrapolation/influence) from original base body." + }, + { + "lineNumber": 61, + "text": "- RetargRelations: skip objects in Original Character's hierarchy (linked collection); only retarget relations outside orig's hierarchy." + }, + { + "lineNumber": 62, + "text": "- MigBoneConst: copy all constraint properties and targets (RNA POINTER/COLLECTION with orig→rep retarget), not just type/name/mute/influence." + }, + { + "lineNumber": 63, + "text": "- AnimLayers: detect/mirror via RNA (obj.als.turn_on) so \"Animation Layer attributes migrated\" reports correctly." + }, + { + "lineNumber": 64, + "text": "- MigCustProps: recursive id-property copy for nested groups; debug logging for armature/bone keys." + }, + { + "lineNumber": 65 + }, + { + "lineNumber": 66 + }, + { + "lineNumber": 67, + "text": "## [0.1.0] - 2026-02-19" + }, + { + "lineNumber": 68 + }, + { + "lineNumber": 69, + "text": "### Added" + }, + { + "lineNumber": 70, + "text": "- Gitea release workflow and runner docker-compose." + }, + { + "lineNumber": 71, + "text": "- Tweak tools subsection: add/remove/bake constraints for arms, legs, and both; track name and post-clean options." + }, + { + "lineNumber": 72, + "text": "- MigNLA: when no NLA, copy active action and action slot (incl. last_slot_identifier, blend/extrapolation/influence); debug logging for slot migration." + }, + { + "lineNumber": 73, + "text": "- NLAMig: AnimLayers support (mirror `als.turn_on`), strip timing and properties; active-action-only path when no NLA tracks." + }, + { + "lineNumber": 74, + "text": "- Operator icons (CopyAttr, MigNLA, CustProps, BoneConst, RetargRelatives, MigBBodyShapeKeys, pickers, tweak ops)." + }, + { + "lineNumber": 75, + "text": "- BaseBody shapekeys step: prefer original base body's shape-key action slot; library override + editable; copy shape key values." + }, + { + "lineNumber": 76 + }, + { + "lineNumber": 77, + "text": "### Changed" + }, + { + "lineNumber": 78, + "text": "- Operator labels refactored to canonical short names (CopyAttr, MigNLA, MigCustProps, etc.) with `bl_description` on all UI operators." + }, + { + "lineNumber": 79, + "text": "- Migrate BaseBody shapekeys redefined: find base body in hierarchy, override mesh/key data when linked, then apply shape-key action." + }, + { + "lineNumber": 80, + "text": "- Button labels truncated (e.g. \"NLA\", \"BaseBody ShapeKeys\")." + }, + { + "lineNumber": 81 + }, + { + "lineNumber": 82, + "text": "### Fixed" + } + ], + "range": { + "endPosition": { + "line": 82 + }, + "startPosition": { + "line": 46 + } + }, + "relativeWorkspacePath": ".\\CHANGELOG.md", + "signatures": {} + }, + "score": 0.16658806800842285 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 40, + "text": "class DynamicLinkManagerProperties(PropertyGroup):" + }, + { + "lineNumber": 41, + "text": " linked_libraries: CollectionProperty(type=LinkedLibraryItem, name=\"Linked Libraries\")" + }, + { + "lineNumber": 42, + "text": " linked_libraries_index: IntProperty(name=\"Linked Libraries Index\", default=0)" + }, + { + "lineNumber": 43, + "text": " linked_assets_count: IntProperty(name=\"Linked Assets Count\", default=0)" + }, + { + "lineNumber": 44, + "text": " linked_libraries_section_expanded: BoolProperty(" + }, + { + "lineNumber": 45, + "text": " name=\"Linked Libraries Analysis Expanded\"," + }, + { + "lineNumber": 46, + "text": " description=\"Show or hide the Linked Libraries Analysis section\"," + }, + { + "lineNumber": 47, + "text": " default=False," + }, + { + "lineNumber": 48, + "text": " )" + }, + { + "lineNumber": 49, + "text": " selected_asset_path: StringProperty(name=\"Selected Asset Path\", default=\"\")" + }, + { + "lineNumber": 50 + }, + { + "lineNumber": 51, + "text": " # Character migrator: rig family (future ARP-specific migration; Remove Original uses it for collection resolution)" + }, + { + "lineNumber": 52, + "text": " migrator_rig_family: EnumProperty(" + }, + { + "lineNumber": 53, + "text": " name=\"Rig family\"," + }, + { + "lineNumber": 54, + "text": " description=\"Rigify vs Auto-Rig Pro: affects Remove Original collection detection and future bone heuristics\"," + }, + { + "lineNumber": 55, + "text": " items=(" + }, + { + "lineNumber": 56, + "text": " (\"RIGIFY\", \"Rigify\", \"Rigify rig naming and defaults\")," + }, + { + "lineNumber": 57, + "text": " (\"ARP\", \"ARP\", \"Auto-Rig Pro rig naming and collection behavior\")," + }, + { + "lineNumber": 58, + "text": " )," + }, + { + "lineNumber": 59, + "text": " default=\"RIGIFY\"," + }, + { + "lineNumber": 60, + "text": " )" + }, + { + "lineNumber": 61 + }, + { + "lineNumber": 62, + "text": " # Character migrator (manual mode)" + }, + { + "lineNumber": 63, + "text": " migrator_mode: BoolProperty(" + }, + { + "lineNumber": 64, + "text": " name=\"Automatic\"," + }, + { + "lineNumber": 65, + "text": " description=\"Automatic: discover pair by Name_Rigify / Name_Rigify.001. Manual: use fields below\"," + }, + { + "lineNumber": 66, + "text": " default=False," + }, + { + "lineNumber": 67, + "text": " )" + }, + { + "lineNumber": 68, + "text": " original_character: PointerProperty(" + }, + { + "lineNumber": 69, + "text": " name=\"Original Character\"," + }, + { + "lineNumber": 70, + "text": " description=\"Armature to migrate from\"," + }, + { + "lineNumber": 71, + "text": " type=bpy.types.Object," + }, + { + "lineNumber": 72, + "text": " poll=lambda self, obj: obj and obj.type == \"ARMATURE\"," + }, + { + "lineNumber": 73, + "text": " )" + }, + { + "lineNumber": 74, + "text": " replacement_character: PointerProperty(" + }, + { + "lineNumber": 75, + "text": " name=\"Replacement Character\"," + }, + { + "lineNumber": 76, + "text": " description=\"Armature to migrate to\"," + }, + { + "lineNumber": 77, + "text": " type=bpy.types.Object," + }, + { + "lineNumber": 78, + "text": " poll=lambda self, obj: obj and obj.type == \"ARMATURE\"," + }, + { + "lineNumber": 79, + "text": " )" + }, + { + "lineNumber": 80 + }, + { + "lineNumber": 81, + "text": " # MigBBody: manual mesh pair when CC/iClone-style auto-detection fails" + } + ], + "range": { + "endPosition": { + "line": 117 + }, + "startPosition": { + "column": 93, + "line": 36 + } + }, + "relativeWorkspacePath": ".\\ui\\properties.py", + "signatures": {} + }, + "score": 0.16007490456104279 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 501, + "text": "def run_mig_bone_const(orig, rep, orig_to_rep):" + }, + { + "lineNumber": 502, + "text": " \"\"\"Bone constraints: remove stale on rep, copy from orig with full props (targets, etc.) and retarget, trim duplicates.\"\"\"" + }, + { + "lineNumber": 503, + "text": " other_originals = [o for o in orig_to_rep if o != orig]" + }, + { + "lineNumber": 504, + "text": " for pb in rep.pose.bones:" + }, + { + "lineNumber": 505, + "text": " to_remove = [c for c in pb.constraints if getattr(c, \"target\", None) in other_originals]" + }, + { + "lineNumber": 506, + "text": " for c in to_remove:" + }, + { + "lineNumber": 507, + "text": " pb.constraints.remove(c)" + }, + { + "lineNumber": 508, + "text": " for pbone in orig.pose.bones:" + }, + { + "lineNumber": 509, + "text": " if pbone.name not in rep.pose.bones:" + }, + { + "lineNumber": 510, + "text": " continue" + }, + { + "lineNumber": 511, + "text": " rbone = rep.pose.bones[pbone.name]" + }, + { + "lineNumber": 512, + "text": " for c in pbone.constraints:" + }, + { + "lineNumber": 513, + "text": " nc = rbone.constraints.new(type=c.type)" + }, + { + "lineNumber": 514, + "text": " nc.name = c.name" + }, + { + "lineNumber": 515, + "text": " _copy_constraint_props(c, nc, orig, rep, orig_to_rep)" + }, + { + "lineNumber": 516, + "text": " for pb in orig.pose.bones:" + }, + { + "lineNumber": 517, + "text": " if pb.name not in rep.pose.bones:" + }, + { + "lineNumber": 518, + "text": " continue" + }, + { + "lineNumber": 519, + "text": " ro, rr = pb.constraints, rep.pose.bones[pb.name].constraints" + }, + { + "lineNumber": 520, + "text": " while len(rr) \u003e len(ro):" + }, + { + "isSignature": true, + "lineNumber": 521, + "text": " rr.remove(rr[-1])" + }, + { + "lineNumber": 522 + }, + { + "lineNumber": 523 + }, + { + "isSignature": true, + "lineNumber": 524, + "text": "def run_retarg_relatives(orig, rep, rep_descendants, orig_to_rep):" + }, + { + "lineNumber": 525, + "text": " \"\"\"" + } + ], + "range": { + "endPosition": { + "line": 575 + }, + "startPosition": { + "column": 20, + "line": 497 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.15677329897880554 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 632, + "text": "def _process_mig_bbody_mesh(orig_base, ob, context):" + }, + { + "lineNumber": 678 + }, + { + "lineNumber": 679, + "text": " if ob.data.shape_keys:" + }, + { + "lineNumber": 680, + "text": " # Ensure we can write shape key values: override the Key block if it is linked." + }, + { + "lineNumber": 681, + "text": " sk = ob.data.shape_keys" + }, + { + "lineNumber": 682, + "text": " if getattr(sk, \"library\", None):" + }, + { + "lineNumber": 683, + "text": " try:" + }, + { + "lineNumber": 684, + "text": " sk.override_create(remap_local_usages=True)" + }, + { + "lineNumber": 685, + "text": " except Exception as e:" + }, + { + "lineNumber": 686, + "text": " print(f\"[DLM step6] {ob.name} shape_keys.override_create: {e}\")" + }, + { + "lineNumber": 687, + "text": " # Copy shape key values from original base body to replacement (by matching key name)." + }, + { + "lineNumber": 688, + "text": " if orig_base and orig_base.data.shape_keys:" + }, + { + "lineNumber": 689, + "text": " rep_blocks = ob.data.shape_keys.key_blocks" + }, + { + "lineNumber": 690, + "text": " orig_blocks = orig_base.data.shape_keys.key_blocks" + }, + { + "lineNumber": 691, + "text": " n_copied = 0" + }, + { + "lineNumber": 692, + "text": " for orig_key in orig_blocks:" + }, + { + "lineNumber": 693, + "text": " rep_key = rep_blocks.get(orig_key.name)" + }, + { + "lineNumber": 694, + "text": " if rep_key is not None:" + }, + { + "lineNumber": 695, + "text": " rep_key.value = orig_key.value" + }, + { + "lineNumber": 696, + "text": " n_copied += 1" + }, + { + "lineNumber": 697, + "text": " print(f\"[DLM step6] {ob.name} shapekey values: copied {n_copied}/{len(orig_blocks)} from {orig_base.name}\")" + }, + { + "lineNumber": 698, + "text": " else:" + }, + { + "lineNumber": 699, + "text": " if not orig_base:" + }, + { + "lineNumber": 700, + "text": " print(f\"[DLM step6] {ob.name} no orig base body found\")" + }, + { + "lineNumber": 701, + "text": " elif not orig_base.data.shape_keys:" + }, + { + "lineNumber": 702, + "text": " print(f\"[DLM step6] {ob.name} orig base body has no shape_keys\")" + }, + { + "lineNumber": 703, + "text": " if not ob.data.shape_keys.animation_data:" + }, + { + "lineNumber": 704, + "text": " ob.data.shape_keys.animation_data_create()" + }, + { + "lineNumber": 705, + "text": " sk_ad = ob.data.shape_keys.animation_data" + }, + { + "lineNumber": 706, + "text": " # Prefer action (and slot) from original base body; fallback to name lookup." + }, + { + "lineNumber": 707, + "text": " orig_sk_ad = None" + }, + { + "lineNumber": 708, + "text": " if orig_base and orig_base.data.shape_keys:" + }, + { + "lineNumber": 709, + "text": " orig_sk_ad = orig_base.data.shape_keys.animation_data" + }, + { + "lineNumber": 710, + "text": " action = None" + }, + { + "lineNumber": 711, + "text": " if orig_sk_ad and getattr(orig_sk_ad, \"action\", None):" + }, + { + "lineNumber": 712, + "text": " action = orig_sk_ad.action" + }, + { + "lineNumber": 713, + "text": " if action is None:" + }, + { + "lineNumber": 714, + "text": " body_name = ob.name" + }, + { + "lineNumber": 715, + "text": " action =" + } + ], + "range": { + "endPosition": { + "line": 736 + }, + "startPosition": { + "line": 631 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": { + "ranges": [ + { + "endPosition": { + "column": 5, + "line": 633 + }, + "startPosition": { + "column": 1, + "line": 632 + } + } + ] + } + }, + "score": 0.1539946049451828 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 595, + "text": "def _find_base_body(armature, descendants_iter, rep_base_name=None):" + }, + { + "lineNumber": 596, + "text": " \"\"\"Return the base body mesh: in descendants (armature mod), or in armature's collection(s), matched by name.\"\"\"" + }, + { + "lineNumber": 597, + "text": " def gather_candidates(ob_iter):" + }, + { + "lineNumber": 598, + "text": " candidates = []" + }, + { + "lineNumber": 599, + "text": " for ob in ob_iter:" + }, + { + "lineNumber": 600, + "text": " if not _base_body_name_match(ob):" + }, + { + "lineNumber": 601, + "text": " continue" + }, + { + "lineNumber": 602, + "text": " if ob.modifiers:" + }, + { + "lineNumber": 603, + "text": " for m in ob.modifiers:" + }, + { + "lineNumber": 604, + "text": " if m.type == \"ARMATURE\" and m.object == armature:" + }, + { + "lineNumber": 605, + "text": " return ob, candidates" + }, + { + "lineNumber": 606, + "text": " candidates.append(ob)" + }, + { + "lineNumber": 607, + "text": " return None, candidates" + }, + { + "lineNumber": 608 + }, + { + "lineNumber": 609, + "text": " found, candidates = gather_candidates(descendants_iter)" + }, + { + "lineNumber": 610, + "text": " if found:" + }, + { + "lineNumber": 611, + "text": " return found" + }, + { + "lineNumber": 612, + "text": " # Fallback: base body may be in same collection as armature but not parented to it (e.g. linked)." + }, + { + "lineNumber": 613, + "text": " if not candidates:" + }, + { + "lineNumber": 614, + "text": " for coll in [collection_containing_armature(armature)] + list(getattr(armature, \"users_collection\", []) or []):" + }, + { + "lineNumber": 615, + "text": " if not coll:" + }, + { + "lineNumber": 616, + "text": " continue" + }, + { + "lineNumber": 617, + "text": " found, candidates = gather_candidates(_objects_in_collection_recursive(coll))" + }, + { + "lineNumber": 618, + "text": " if found:" + }, + { + "lineNumber": 619, + "text": " return found" + }, + { + "lineNumber": 620, + "text": " if candidates:" + }, + { + "lineNumber": 621, + "text": " break" + }, + { + "lineNumber": 622, + "text": " if not candidates:" + }, + { + "lineNumber": 623, + "text": " return None" + }, + { + "lineNumber": 624, + "text": " if rep_base_name:" + }, + { + "lineNumber": 625, + "text": " base = rep_base_name.rsplit(\".\", 1)[0] if \".\" in rep_base_name else rep_base_name" + }, + { + "lineNumber": 626, + "text": " for ob in candidates:" + }, + { + "lineNumber": 627, + "text": " if ob.name == base or ob.name.startswith(base + \".\") or (ob.data and ob.data.name == base):" + }, + { + "lineNumber": 628, + "text": " return ob" + }, + { + "isSignature": true, + "lineNumber": 629, + "text": " return candidates[0]" + }, + { + "lineNumber": 630 + }, + { + "lineNumber": 631 + }, + { + "isSignature": true, + "lineNumber": 632, + "text": "def _process_mig_bbody_mesh(orig_base, ob, context):" + }, + { + "lineNumber": 633, + "text": " \"\"\"" + } + ], + "range": { + "endPosition": { + "line": 736 + }, + "startPosition": { + "column": 58, + "line": 591 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.1515406370162964 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 40, + "text": "class DynamicLinkManagerProperties(PropertyGroup):" + }, + { + "lineNumber": 79 + }, + { + "lineNumber": 80 + }, + { + "lineNumber": 81, + "text": " # MigBBody: manual mesh pair when CC/iClone-style auto-detection fails" + }, + { + "lineNumber": 82, + "text": " migbbody_manual_override: BoolProperty(" + }, + { + "lineNumber": 83, + "text": " name=\"Manual body meshes\"," + }, + { + "lineNumber": 84, + "text": " description=(" + }, + { + "lineNumber": 85, + "text": " \"Enable to pick original and replacement body meshes manually. \"" + }, + { + "lineNumber": 86, + "text": " \"Auto-enables if MigBBody cannot find a base mesh (non-CC rigs).\"" + }, + { + "lineNumber": 87, + "text": " )," + }, + { + "lineNumber": 88, + "text": " default=False," + }, + { + "lineNumber": 89, + "text": " )" + }, + { + "lineNumber": 90, + "text": " migbbody_orig_body: PointerProperty(" + }, + { + "lineNumber": 91, + "text": " name=\"Original body\"," + }, + { + "lineNumber": 92, + "text": " description=\"Original character body mesh (shape key source)\"," + }, + { + "lineNumber": 93, + "text": " type=bpy.types.Object," + }, + { + "lineNumber": 94, + "text": " poll=lambda self, obj: obj and obj.type == \"MESH\"," + }, + { + "lineNumber": 95, + "text": " )" + }, + { + "lineNumber": 96, + "text": " migbbody_rep_body: PointerProperty(" + }, + { + "lineNumber": 97, + "text": " name=\"Replacement body\"," + }, + { + "lineNumber": 98, + "text": " description=\"Replacement character body mesh (shape key target)\"," + }, + { + "lineNumber": 99, + "text": " type=bpy.types.Object," + }, + { + "lineNumber": 100, + "text": " poll=lambda self, obj: obj and obj.type == \"MESH\"," + }, + { + "lineNumber": 101, + "text": " )" + }, + { + "lineNumber": 102 + }, + { + "lineNumber": 103, + "text": " # Tweak tools (collapsible section)" + }, + { + "lineNumber": 104, + "text": " tweak_tools_section_expanded: BoolProperty(" + }, + { + "lineNumber": 105, + "text": " name=\"Tweak Tools Expanded\"," + }, + { + "lineNumber": 106, + "text": " description=\"Show or hide the Tweak Tools section\"," + }, + { + "lineNumber": 107, + "text": " default=False," + }, + { + "lineNumber": 108, + "text": " )" + }, + { + "lineNumber": 109, + "text": " tweak_nla_track_name: StringProperty(" + }, + { + "lineNumber": 110, + "text": " name=\"NLA Track (bake range)\"," + }, + { + "lineNumber": 111, + "text": " description=\"If set, bake uses this NLA track on the replacement armature for frame range; else scene range\"," + }, + { + "lineNumber": 112, + "text": " default=\"\"," + }, + { + "lineNumber": 113, + "text": " )" + }, + { + "lineNumber": 114, + "text": " tweak_bake_post_clean: BoolProperty(" + }, + { + "lineNumber": 115, + "text": " name=\"Post-clean after bake\"," + }, + { + "lineNumber": 116, + "text": " description=\"Run action clean keyframes and graph decimate (error 0.001) after baking\"," + }, + { + "lineNumber": 117, + "text": " default=False," + }, + { + "isSignature": true, + "lineNumber": 118, + "text": " )" + } + ], + "range": { + "endPosition": { + "column": 5, + "line": 117 + }, + "startPosition": { + "line": 39 + } + }, + "relativeWorkspacePath": ".\\ui\\properties.py", + "signatures": { + "ranges": [ + { + "endPosition": { + "column": 5, + "line": 41 + }, + "startPosition": { + "column": 1, + "line": 40 + } + } + ] + } + }, + "score": 0.15146060287952423 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 72, + "text": "def copy_fk_rotations(context, orig, rep):" + }, + { + "lineNumber": 93 + }, + { + "lineNumber": 94 + }, + { + "lineNumber": 95, + "text": " try:" + }, + { + "lineNumber": 96, + "text": " # Ensure rep is active and in pose mode" + }, + { + "lineNumber": 97, + "text": " bpy.context.view_layer.objects.active = rep" + }, + { + "lineNumber": 98, + "text": " if rep.mode != 'POSE':" + }, + { + "lineNumber": 99, + "text": " bpy.ops.object.mode_set(mode='POSE')" + }, + { + "lineNumber": 100 + }, + { + "lineNumber": 101, + "text": " # Step 1: Add COPY_TRANSFORMS constraints to each rep bone" + }, + { + "lineNumber": 102, + "text": " for bone_name in common_bones:" + }, + { + "lineNumber": 103, + "text": " rep_bone = rep.pose.bones[bone_name]" + }, + { + "lineNumber": 104, + "text": " " + }, + { + "lineNumber": 105, + "text": " # Check if bone already has this constraint" + }, + { + "lineNumber": 106, + "text": " existing = [c for c in rep_bone.constraints if c.type == 'COPY_TRANSFORMS' and getattr(c, 'target', None) == orig]" + }, + { + "lineNumber": 107, + "text": " if existing:" + }, + { + "lineNumber": 108, + "text": " continue" + }, + { + "lineNumber": 109, + "text": " " + }, + { + "lineNumber": 110, + "text": " # Add constraint" + }, + { + "lineNumber": 111, + "text": " c = rep_bone.constraints.new(type='COPY_TRANSFORMS')" + }, + { + "lineNumber": 112, + "text": " c.name = \"MigFKRot_Temp\"" + }, + { + "lineNumber": 113, + "text": " c.target = orig" + }, + { + "lineNumber": 114, + "text": " c.subtarget = bone_name" + }, + { + "lineNumber": 115, + "text": " c.target_space = 'POSE'" + }, + { + "lineNumber": 116, + "text": " c.owner_space = 'POSE'" + }, + { + "lineNumber": 117, + "text": " constraints_added.append((rep_bone, c))" + }, + { + "lineNumber": 118 + }, + { + "lineNumber": 119, + "text": " # Step 2: Update scene to evaluate constraints" + }, + { + "lineNumber": 120, + "text": " context.view_layer.update()" + }, + { + "lineNumber": 121 + }, + { + "lineNumber": 122, + "text": " # Step 3: Apply visual transform (bake constraint result into pose)" + }, + { + "lineNumber": 123, + "text": " try:" + }, + { + "lineNumber": 124, + "text": " bpy.ops.pose.select_all(action='DESELECT')" + }, + { + "lineNumber": 125, + "text": " for rep_bone, _ in constraints_added:" + }, + { + "lineNumber": 126, + "text": " rep_bone.bone.select = True" + }, + { + "lineNumber": 127, + "text": " # Apply visual transform - this bakes the constraint result" + }, + { + "lineNumber": 128, + "text": " bpy.ops.pose.visual_transform_apply()" + }, + { + "lineNumber": 129, + "text": " except (RuntimeError, AttributeError):" + }, + { + "lineNumber": 130, + "text": " # visual_transform_apply requires bones to be selectable" + }, + { + "lineNumber": 131, + "text": " # AttributeError: 'bone.select' may not exist in Blender 5.0" + }, + { + "lineNumber": 132, + "text": " # If selection fails, the constraint result is still applied" + }, + { + "lineNumber": 133, + "text": " pass # Silently ignore - constraints still drove the pose" + }, + { + "lineNumber": 134 + }, + { + "lineNumber": 135, + "text": " # Constraints remain active for bake step" + }, + { + "lineNumber": 136, + "text": " print(f\"[DLM MigFKRot] Copied {len(constraints_added)} bones (constraints active)\")" + }, + { + "lineNumber": 137, + "text": " return True, f\"Copied FK rotations for {len(constraints_added)} bones (constraints active - run Bake to finalize)\"" + }, + { + "lineNumber": 138 + }, + { + "lineNumber": 139, + "text": " except Exception as e:" + }, + { + "lineNumber": 140, + "text": " print(f\"[DLM MigFKRot] Error: {e}\")" + } + ], + "range": { + "endPosition": { + "line": 152 + }, + "startPosition": { + "line": 71 + } + }, + "relativeWorkspacePath": ".\\ops\\fk_rotations.py", + "signatures": { + "ranges": [ + { + "endPosition": { + "column": 5, + "line": 73 + }, + "startPosition": { + "column": 1, + "line": 72 + } + } + ] + } + }, + "score": 0.1493133008480072 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 72, + "text": "def copy_fk_rotations(context, orig, rep):" + }, + { + "lineNumber": 73, + "text": " \"\"\"" + }, + { + "lineNumber": 74, + "text": " Copy visual rotations from orig to rep using temporary COPY_TRANSFORMS constraints." + }, + { + "lineNumber": 75, + "text": " This properly handles all coordinate space conversions." + }, + { + "lineNumber": 76, + "text": " Returns (True, message) or (False, error_message)." + }, + { + "lineNumber": 77, + "text": " \"\"\"" + }, + { + "lineNumber": 78, + "text": " fk_names = _get_fk_bones(rep)" + }, + { + "lineNumber": 79 + }, + { + "lineNumber": 80, + "text": " print(f\"[DLM MigFKRot] Found {len(fk_names)} FK bones on {rep.name}\")" + }, + { + "lineNumber": 81 + }, + { + "lineNumber": 82, + "text": " if not fk_names:" + }, + { + "lineNumber": 83, + "text": " return False, \"No FK arm or finger bones found on replacement armature\"" + }, + { + "lineNumber": 84 + }, + { + "lineNumber": 85, + "text": " # Filter to bones that exist on both" + }, + { + "lineNumber": 86, + "text": " common_bones = [n for n in fk_names if n in orig.pose.bones and n in rep.pose.bones]" + }, + { + "lineNumber": 87, + "text": " if not common_bones:" + }, + { + "lineNumber": 88, + "text": " return False, \"No matching FK bones found on both armatures\"" + }, + { + "lineNumber": 89 + }, + { + "lineNumber": 90, + "text": " print(f\"[DLM MigFKRot] Will copy {len(common_bones)} bones using constraints\")" + }, + { + "lineNumber": 91 + }, + { + "lineNumber": 92, + "text": " original_active = context.view_layer.objects.active" + }, + { + "lineNumber": 93, + "text": " constraints_added = []" + }, + { + "lineNumber": 94 + }, + { + "lineNumber": 95, + "text": " try:" + }, + { + "lineNumber": 96, + "text": " # Ensure rep is active and in pose mode" + }, + { + "lineNumber": 97, + "text": " bpy.context.view_layer.objects.active = rep" + } + ], + "range": { + "endPosition": { + "line": 152 + }, + "startPosition": { + "column": 56, + "line": 68 + } + }, + "relativeWorkspacePath": ".\\ops\\fk_rotations.py", + "signatures": {} + }, + "score": 0.14926773309707642 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 25, + "text": "def scan_linked_assets(context, report):" + }, + { + "lineNumber": 26, + "text": " props = context.scene.dynamic_link_manager" + }, + { + "lineNumber": 27, + "text": " props.linked_libraries.clear()" + }, + { + "lineNumber": 28 + }, + { + "lineNumber": 29, + "text": " for lib in bpy.data.libraries:" + }, + { + "lineNumber": 30, + "text": " try:" + }, + { + "lineNumber": 31, + "text": " if lib.filepath:" + }, + { + "lineNumber": 32, + "text": " lib.reload()" + }, + { + "lineNumber": 33, + "text": " except Exception:" + }, + { + "lineNumber": 34, + "text": " pass" + }, + { + "lineNumber": 35 + }, + { + "lineNumber": 36, + "text": " direct_libs = set()" + }, + { + "lineNumber": 37, + "text": " for lib in bpy.data.libraries:" + }, + { + "lineNumber": 38, + "text": " try:" + }, + { + "lineNumber": 39, + "text": " if getattr(lib, \"parent\", None) is None and lib.filepath:" + }, + { + "lineNumber": 40, + "text": " direct_libs.add(lib.filepath)" + }, + { + "lineNumber": 41, + "text": " except Exception:" + }, + { + "lineNumber": 42, + "text": " continue" + }, + { + "lineNumber": 43, + "text": " all_libraries = set(direct_libs)" + }, + { + "lineNumber": 44, + "text": " props.linked_assets_count = len(all_libraries)" + }, + { + "lineNumber": 45 + }, + { + "lineNumber": 46, + "text": " missing_indirect_libs = set()" + }, + { + "lineNumber": 47, + "text": " for lib in bpy.data.libraries:" + }, + { + "lineNumber": 48, + "text": " try:" + }, + { + "lineNumber": 49, + "text": " if getattr(lib, \"parent\", None) is not None and lib.filepath:" + }, + { + "lineNumber": 50, + "text": " try:" + }, + { + "lineNumber": 51, + "text": " abs_child = bpy.path.abspath(lib.filepath)" + }, + { + "lineNumber": 52, + "text": " except Exception:" + }, + { + "lineNumber": 53, + "text": " abs_child = lib.filepath" + }, + { + "lineNumber": 54, + "text": " if not os.path.isfile(abs_child):" + }, + { + "lineNumber": 55, + "text": " root = lib.parent" + }, + { + "lineNumber": 56, + "text": " while getattr(root, \"parent\", None) is not None:" + }, + { + "lineNumber": 57, + "text": " root = root.parent" + }, + { + "lineNumber": 58, + "text": " if root and root.filepath:" + }, + { + "lineNumber": 59, + "text": " missing_indirect_libs.add(root.filepath)" + }, + { + "lineNumber": 60, + "text": " except Exception:" + }, + { + "lineNumber": 61, + "text": " continue" + }, + { + "lineNumber": 62 + }, + { + "lineNumber": 63, + "text": " missing_ids_by_library = set()" + }, + { + "lineNumber": 64, + "text": " for idb in list(bpy.data.objects) + list(bpy.data.meshes) + list(bpy.data.armatures) + list(bpy.data.materials) + list(bpy.data.node_groups) + list(bpy.data.images) + list(bpy.data.texts) + list(bpy.data.collections) + list(bpy.data.cameras) + list(bpy.data.lights):" + }, + { + "lineNumber": 65, + "text": " try:" + }, + { + "lineNumber": 66, + "text": " lib = getattr(idb, \"library\", None)" + }, + { + "lineNumber": 67, + "text": " if lib and lib.filepath and getattr(idb, \"is_library_missing\", False):" + }, + { + "lineNumber": 68, + "text": " missing_ids_by_library.add(lib.filepath)" + }, + { + "lineNumber": 69, + "text": " except Exception:" + }, + { + "lineNumber": 70, + "text": " continue" + }, + { + "lineNumber": 71 + }, + { + "lineNumber": 72, + "text": " library_items = []" + }, + { + "lineNumber": 73, + "text": " for filepath in sorted(all_libraries):" + }, + { + "lineNumber": 74, + "text": " if not filepath:" + }, + { + "lineNumber": 75, + "text": " continue" + } + ], + "range": { + "endPosition": { + "line": 92 + }, + "startPosition": { + "column": 64, + "line": 21 + } + }, + "relativeWorkspacePath": ".\\ops\\library.py", + "signatures": {} + }, + "score": 0.14071379601955414 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 632, + "text": "def _process_mig_bbody_mesh(orig_base, ob, context):" + }, + { + "lineNumber": 633, + "text": " \"\"\"Library overrides on rep mesh ob; copy shape key values and action from orig_base mesh.\"\"\"" + }, + { + "lineNumber": 634, + "text": " # Debug: base body mesh state before override handling." + }, + { + "lineNumber": 635, + "text": " _lib = getattr(ob.data, \"library\", None)" + }, + { + "lineNumber": 636, + "text": " _ol = getattr(ob.data, \"override_library\", None)" + }, + { + "lineNumber": 637, + "text": " _sys = getattr(_ol, \"is_system_override\", None) if _ol else None" + }, + { + "lineNumber": 638, + "text": " print(f\"[DLM step6] {ob.name} data: linked={_lib is not None}, override={_ol is not None}, is_system_override={_sys}\")" + }, + { + "lineNumber": 639, + "text": " # Library override: use hierarchy create (fully editable) when context available, else single-id override." + }, + { + "lineNumber": 640, + "text": " if getattr(ob, \"library\", None):" + }, + { + "lineNumber": 641, + "text": " if context:" + }, + { + "lineNumber": 642, + "text": " try:" + }, + { + "lineNumber": 643, + "text": " ob = ob.override_hierarchy_create(" + }, + { + "lineNumber": 644, + "text": " context.scene, context.view_layer, do_fully_editable=True" + }, + { + "lineNumber": 645, + "text": " )" + }, + { + "lineNumber": 646, + "text": " except Exception:" + }, + { + "lineNumber": 647, + "text": " try:" + }, + { + "lineNumber": 648, + "text": " ob.override_create()" + }, + { + "lineNumber": 649, + "text": " except Exception:" + }, + { + "lineNumber": 650, + "text": " pass" + }, + { + "lineNumber": 651, + "text": " else:" + }, + { + "lineNumber": 652, + "text": " try:" + }, + { + "lineNumber": 653, + "text": " ob.override_create()" + }, + { + "lineNumber": 654, + "text": " except Exception:" + }, + { + "lineNumber": 655, + "text": " pass" + }, + { + "lineNumber": 656, + "text": " if getattr(ob.data, \"library\", None):" + }, + { + "lineNumber": 657, + "text": " try:" + }, + { + "lineNumber": 658, + "text": " ob.data.override_create(remap_local_usages=True)" + } + ], + "range": { + "endPosition": { + "line": 736 + }, + "startPosition": { + "column": 24, + "line": 628 + } + }, + "relativeWorkspacePath": ".\\ops\\migrator.py", + "signatures": {} + }, + "score": 0.14053523540496826 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 199, + "text": "def bake_fk_rotations(context, orig, rep, track_name=None, post_clean=False):" + }, + { + "lineNumber": 285 + }, + { + "lineNumber": 286, + "text": " try:" + }, + { + "lineNumber": 287, + "text": " # Deselect all bones first" + }, + { + "lineNumber": 288, + "text": " bpy.ops.pose.select_all(action='DESELECT')" + }, + { + "lineNumber": 289, + "text": " " + }, + { + "lineNumber": 290, + "text": " # Select only our FK bones" + }, + { + "lineNumber": 291, + "text": " for bone_name in common_bones:" + }, + { + "lineNumber": 292, + "text": " if bone_name in rep.pose.bones:" + }, + { + "lineNumber": 293, + "text": " rep_bone = rep.pose.bones[bone_name]" + }, + { + "lineNumber": 294, + "text": " # Try different ways to select the bone (Blender 5.0 compatibility)" + }, + { + "lineNumber": 295, + "text": " try:" + }, + { + "lineNumber": 296, + "text": " # Method 1: Direct selection on pose bone" + }, + { + "lineNumber": 297, + "text": " rep_bone.select = True" + }, + { + "lineNumber": 298, + "text": " except (AttributeError, TypeError):" + }, + { + "lineNumber": 299, + "text": " try:" + }, + { + "lineNumber": 300, + "text": " # Method 2: Selection on the bone property" + }, + { + "lineNumber": 301, + "text": " if hasattr(rep_bone, 'bone'):" + }, + { + "lineNumber": 302, + "text": " rep_bone.bone.select = True" + }, + { + "lineNumber": 303, + "text": " except (AttributeError, TypeError):" + }, + { + "lineNumber": 304, + "text": " pass # Selection failed, continue anyway" + }, + { + "lineNumber": 305, + "text": " " + }, + { + "lineNumber": 306, + "text": " print(f\"[DLM MigFKRot Bake] Running nla.bake with only_selected=True...\")" + }, + { + "lineNumber": 307, + "text": " bpy.ops.nla.bake(" + }, + { + "lineNumber": 308, + "text": " frame_start=frame_start," + }, + { + "lineNumber": 309, + "text": " frame_end=frame_end," + }, + { + "lineNumber": 310, + "text": " step=1," + }, + { + "lineNumber": 311, + "text": " only_selected=True," + }, + { + "lineNumber": 312, + "text": " visual_keying=True," + }, + { + "lineNumber": 313, + "text": " clear_constraints=False," + }, + { + "lineNumber": 314, + "text": " clear_parents=False," + }, + { + "lineNumber": 315, + "text": " use_current_action=True," + }, + { + "lineNumber": 316, + "text": " clean_curves=False," + }, + { + "lineNumber": 317, + "text": " bake_types={\"POSE\"}," + }, + { + "lineNumber": 318, + "text": " channel_types={\"ROTATION\"}," + }, + { + "lineNumber": 319, + "text": " )" + }, + { + "lineNumber": 320, + "text": " print(f\"[DLM MigFKRot Bake] nla.bake completed successfully\")" + }, + { + "lineNumber": 321, + "text": " except Exception as e:" + }, + { + "lineNumber": 322, + "text": " # Clean up constraints on failure" + }, + { + "lineNumber": 323, + "text": " for rep_bone, c in constraints_added:" + }, + { + "lineNumber": 324, + "text": " try:" + }, + { + "lineNumber": 325, + "text": " if c in rep_bone.constraints:" + }, + { + "lineNumber": 326, + "text": " rep_bone.constraints.remove(c)" + }, + { + "lineNumber": 327, + "text": " except:" + }, + { + "lineNumber": 328, + "text": " pass" + }, + { + "lineNumber": 329, + "text": " return False, f\"nla.bake failed: {e}\"" + }, + { + "lineNumber": 330, + "text": " " + }, + { + "lineNumber": 331, + "text": " # Step 4: Remove constraints" + }, + { + "lineNumber": 332, + "text": " removed_count = 0" + }, + { + "lineNumber": 333, + "text": " for rep_bone, c in constraints_added:" + }, + { + "lineNumber": 334, + "text": " try:" + }, + { + "lineNumber": 335, + "text": " if c in rep_bone.constraints:" + } + ], + "range": { + "endPosition": { + "line": 376 + }, + "startPosition": { + "line": 198 + } + }, + "relativeWorkspacePath": ".\\ops\\fk_rotations.py", + "signatures": { + "ranges": [ + { + "endPosition": { + "column": 5, + "line": 200 + }, + "startPosition": { + "column": 1, + "line": 199 + } + } + ] + } + }, + "score": 0.1404009461402893 + }, + { + "codeBlock": { + "detailedLines": [ + { + "isSignature": true, + "lineNumber": 56, + "text": "class DLM_PT_main_panel(Panel):" + }, + { + "lineNumber": 57, + "text": " bl_space_type = \"VIEW_3D\"" + }, + { + "lineNumber": 58, + "text": " bl_region_type = \"UI\"" + }, + { + "lineNumber": 59, + "text": " bl_category = \"Dynamic Link Manager\"" + }, + { + "lineNumber": 60, + "text": " bl_label = \"Dynamic Link Manager\"" + }, + { + "lineNumber": 61 + }, + { + "lineNumber": 62, + "text": " def draw_header(self, context):" + }, + { + "lineNumber": 63, + "text": " layout = self.layout" + }, + { + "lineNumber": 64, + "text": " layout.operator(\"preferences.addon_show\", text=\"\", icon=\"PREFERENCES\").module = __package__.rsplit(\".\", 1)[0]" + }, + { + "lineNumber": 65 + }, + { + "lineNumber": 66, + "text": " def draw(self, context):" + }, + { + "lineNumber": 67, + "text": " layout = self.layout" + }, + { + "lineNumber": 68, + "text": " props = context.scene.dynamic_link_manager" + }, + { + "lineNumber": 69 + }, + { + "lineNumber": 70, + "text": " # Path management" + }, + { + "lineNumber": 71, + "text": " row = layout.row()" + }, + { + "lineNumber": 72, + "text": " row.operator(\"dlm.make_paths_relative\", text=\"Make Paths Relative\", icon=\"FILE_PARENT\")" + }, + { + "lineNumber": 73, + "text": " row.operator(\"dlm.make_paths_absolute\", text=\"Make Paths Absolute\", icon=\"FILE_FOLDER\")" + }, + { + "lineNumber": 74 + }, + { + "lineNumber": 75, + "text": " # CharMig section (placeholder; full UI in implementation step 5)" + }, + { + "lineNumber": 76, + "text": " box = layout.box()" + }, + { + "lineNumber": 77, + "text": " box.label(text=\"Character Migrator\")" + }, + { + "lineNumber": 78, + "text": " row = box.row()" + }, + { + "lineNumber": 79, + "text": " row.prop(props, \"migrator_rig_family\", expand=True)" + }, + { + "lineNumber": 80, + "text": " row = box.row()" + }, + { + "lineNumber": 81, + "text": " row.prop(props, \"migrator_mode\", text=\"Automatic pair discovery\")" + }, + { + "lineNumber": 82, + "text": " row = box.row()" + }, + { + "lineNumber": 83, + "text": " row.prop(props, \"original_character\", text=\"Original\")" + }, + { + "lineNumber": 84, + "text": " row.operator(\"dlm.picker_original_character\", text=\"\", icon=\"EYEDROPPER\")" + }, + { + "lineNumber": 85, + "text": " row = box.row()" + }, + { + "lineNumber": 86, + "text": " row.prop(props, \"replacement_character\", text=\"Replacement\")" + }, + { + "lineNumber": 87, + "text": " row.operator(\"dlm.picker_replacement_character\", text=\"\", icon=\"EYEDROPPER\")" + }, + { + "lineNumber": 88, + "text": " row = box.row()" + }, + { + "lineNumber": 89, + "text": " row.operator(\"dlm.migrator_remove_original\", text=\"Remove Original\", icon=\"TRASH\")" + }, + { + "lineNumber": 90, + "text": " row = box.row(align=True)" + }, + { + "lineNumber": 91, + "text": " row.operator(\"dlm.migrator_copy_attributes\", text=\"CopyAttr\", icon=\"COPY_ID\")" + }, + { + "lineNumber": 92, + "text": " row.operator(\"dlm.migrator_migrate_nla\", text=\"MigNLA\", icon=\"NLA\")" + }, + { + "lineNumber": 93, + "text": " row.operator(\"dlm.migrator_custom_properties\", text=\"MigCustProps\", icon=\"PROPERTIES\")" + }, + { + "lineNumber": 94, + "text": " row = box.row(align=True)" + }, + { + "lineNumber": 95, + "text": " row.operator(\"dlm.migrator_bone_constraints\", text=\"MigBoneConst\", icon=\"CONSTRAINT_BONE\")" + }, + { + "lineNumber": 96, + "text": " row.operator(\"dlm.migrator_retarget_relations\", text=\"RetargRelatives\", icon=\"ORIENTATION_PARENT\")" + }, + { + "lineNumber": 97 + }, + { + "lineNumber": 98, + "text": " # Situational" + }, + { + "lineNumber": 99, + "text": " situational_box = layout.box()" + } + ], + "range": { + "endPosition": { + "line": 190 + }, + "startPosition": { + "column": 20, + "line": 52 + } + }, + "relativeWorkspacePath": ".\\ui\\panels.py", + "signatures": {} + }, + "score": 0.13813793659210205 + } + ] +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **semantic_search_full** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Searching scene objects** + +I'm searching for objects containing 'broom' in their name and relevant armatures using Blender Python. + +**Analyzing reparenting logic** + +I discovered that objects under the original in hierarchy get removed from candidates and never reparented. + +**Processing parent retarget** + +I'm adjusting the parent retarget logic to handle cases where objects are indirectly parented through intermediate objects like empties. I'm checking if intermediates like empties are in candidates to ensure proper retargeting of child objects. I'm excluding descendants of orig from candidates to prevent parent retargeting within the original hierarchy.
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "caseInsensitive": false, + "path": "T:/Dynamic-Link-Manager", + "pattern": "def descendants" +} +``` +Additional data: + +```json +{ + "isPruned": true, + "outputMode": "content", + "path": "T:/Dynamic-Link-Manager", + "pattern": "def descendants", + "topFiles": [ + { + "matchCount": 1, + "uri": ".\\utils\\__init__.py" + }, + { + "matchCount": 1, + "uri": ".\\.specstory\\history\\2026-03-10_16-23-09Z-dlm_ot_migrator_remove_original-class-update.md" + }, + { + "matchCount": 2, + "uri": ".\\.specstory\\history\\2026-02-18_23-39-18Z-character-migrator-workflow-integration.md" + }, + { + "matchCount": 1, + "uri": ".\\.specstory\\history\\2026-03-10_16-22-17Z-dlm_ot_migrator_remove_original-class-implementation.md" + }, + { + "matchCount": 2, + "uri": ".\\.specstory\\history\\2026-02-18_23-39Z-character-migrator-workflow-integration.md" + }, + { + "matchCount": 1, + "uri": ".\\.specstory\\history\\2026-03-10_16-23Z-dlm-ot-migrator-remove-original-class-update.md" + } + ], + "totalFiles": 6, + "totalMatches": 8 +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **ripgrep_raw_search** + +
+ +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n# Find broom / armature / constraint patterns for debugging\\nobjs = [o for o in bpy.data.objects if \\\"broom\\\" in o.name.lower() or \\\"Broom\\\" in o.name]\\nout = []\\nfor o in objs[:50]:\\n p = o.parent\\n out.append({\\n \\\"name\\\": o.name,\\n \\\"type\\\": o.type,\\n \\\"parent\\\": p.name if p else None,\\n \\\"parent_bone\\\": getattr(o, \\\"parent_bone\\\", None),\\n })\\nprint(\\\"BROOM_MATCH\\\", len(objs))\\nfor row in out[:30]:\\n print(row)\\n\\n# Armatures in scene\\narms = [o for o in bpy.data.objects if o.type == \\\"ARMATURE\\\"]\\nprint(\\\"ARMATURES\\\", len(arms), [a.name for a in arms[:25]])\\n\",\"user_prompt\":\"Find power broom and armatures for RetargRelatives debug\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: BROOM_MATCH 6\\n{'name': 'Broom.Hair.001', 'type': 'MESH', 'parent': 'Push Broom.001', 'parent_bone': ''}\\n{'name': 'Power Broom.001', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\n{'name': 'Push Broom.001', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\n{'name': 'Power Broom', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\n{'name': 'Broom.Hair.001', 'type': 'MESH', 'parent': 'Push Broom', 'parent_bone': ''}\\n{'name': 'Push Broom', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\nARMATURES 3 ['rig', 'rig.001', 'rig']\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: BROOM_MATCH 6\\n{'name': 'Broom.Hair.001', 'type': 'MESH', 'parent': 'Push Broom.001', 'parent_bone': ''}\\n{'name': 'Power Broom.001', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\n{'name': 'Push Broom.001', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\n{'name': 'Power Broom', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\n{'name': 'Broom.Hair.001', 'type': 'MESH', 'parent': 'Push Broom', 'parent_bone': ''}\\n{'name': 'Push Broom', 'type': 'MESH', 'parent': None, 'parent_bone': ''}\\nARMATURES 3 ['rig', 'rig.001', 'rig']\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
Tool use: **read_file_v2** • Read file: T:/Dynamic-Link-Manager/utils/__init__.py + +
+
+ +
+Tool use: **read_file_v2** + +
+ +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\ndef dump_obj(ob):\\n lines = [f\\\"=== {ob.name} ({ob.type}) parent={ob.parent.name if ob.parent else None}\\\"]\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n sub = getattr(c, \\\"subtarget\\\", \\\"\\\")\\n lines.append(f\\\" cons: {c.type} target={t.name if t else None} sub={sub!r}\\\")\\n if ob.type == \\\"MESH\\\":\\n for m in ob.modifiers:\\n o = getattr(m, \\\"object\\\", None)\\n if o:\\n lines.append(f\\\" mod: {m.type} object={o.name}\\\")\\n elif m.type == \\\"ARMATURE\\\":\\n lines.append(f\\\" mod: ARMATURE obj={getattr(m, 'object', None)}\\\")\\n return lines\\n\\nfor name in [\\\"Power Broom\\\", \\\"Power Broom.001\\\", \\\"Push Broom\\\", \\\"Push Broom.001\\\"]:\\n ob = bpy.data.objects.get(name)\\n if ob:\\n print(\\\"\\\\n\\\".join(dump_obj(ob)))\\n\\n# Which rig is which - library\\nfor name in [\\\"rig\\\", \\\"rig.001\\\"]:\\n obs = [o for o in bpy.data.objects if o.name == name and o.type == \\\"ARMATURE\\\"]\\n for o in obs:\\n print(\\\"ARM\\\", o.name, \\\"lib\\\", o.library.filepath if o.library else \\\"LOCAL\\\", \\\"users\\\", len(o.users_collection))\\n\",\"user_prompt\":\"Dump Power Broom constraints and rig objects\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: === Power Broom (MESH) parent=None\\n=== Power Broom.001 (MESH) parent=None\\n cons: CHILD_OF target=Empty.001 sub=''\\n=== Push Broom (MESH) parent=None\\n=== Push Broom.001 (MESH) parent=None\\nARM rig lib LOCAL users 1\\nARM rig lib //..\\\\..\\\\..\\\\Assets\\\\Blends\\\\Char\\\\TF-rig_2.03.blend users 1\\nARM rig.001 lib LOCAL users 1\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: === Power Broom (MESH) parent=None\\n=== Power Broom.001 (MESH) parent=None\\n cons: CHILD_OF target=Empty.001 sub=''\\n=== Push Broom (MESH) parent=None\\n=== Push Broom.001 (MESH) parent=None\\nARM rig lib LOCAL users 1\\nARM rig lib //..\\\\..\\\\..\\\\Assets\\\\Blends\\\\Char\\\\TF-rig_2.03.blend users 1\\nARM rig.001 lib LOCAL users 1\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Analyzing rig constraints** + +I'm identifying rig constraints by checking parent relationships and constraint targets.
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\ndef all_cons(ob):\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n print(f\\\" {ob.name}: {c.name} {c.type} target={getattr(t, 'name', None)}\\\")\\n\\nfor nm in [\\\"Empty.001\\\", \\\"Empty\\\", \\\"Power Broom\\\", \\\"Power Broom.001\\\"]:\\n o = bpy.data.objects.get(nm)\\n if o:\\n print(\\\"---\\\", nm, \\\"parent\\\", o.parent.name if o.parent else None)\\n all_cons(o)\\n\\n# Any object with constraint target containing rig\\nrig_names = {o.name for o in bpy.data.objects if o.type == \\\"ARMATURE\\\" and \\\"rig\\\" in o.name.lower()}\\nhits = []\\nfor ob in bpy.data.objects:\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n if t and t.name in rig_names:\\n hits.append((ob.name, c.type, c.name, t.name))\\n if ob.type == \\\"ARMATURE\\\" and ob.pose:\\n for b in ob.pose.bones:\\n for c in b.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n if t and t.name in rig_names:\\n hits.append((f\\\"{ob.name}:{b.name}\\\", c.type, c.name, t.name))\\nprint(\\\"CONSTRAINTS_TO_RIG\\\", len(hits))\\nfor h in hits[:40]:\\n print(h)\\n\",\"user_prompt\":\"Trace Empty and constraints targeting rig armatures\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: --- Empty.001 parent None\\n Empty.001: Track To TRACK_TO target=Empty\\n--- Empty parent Empty.001\\n--- Power Broom parent None\\n--- Power Broom.001 parent None\\n Power Broom.001: Child_Of_Empty.001 CHILD_OF target=Empty.001\\nCONSTRAINTS_TO_RIG 741\\n('rig:root.x', 'COPY_TRANSFORMS', 'Copy Transforms_FWD', 'rig')\\n('rig:root.x', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:thigh_ik.l', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:thigh_ik.l', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.l', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.l', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:leg_ik.l', 'IK', 'autostretch', 'rig')\\n('rig:foot.l', 'COPY_ROTATION', 'rotIK', 'rig')\\n('rig:foot.l', 'COPY_SCALE', 'scaleIK', 'rig')\\n('rig:foot.l', 'COPY_ROTATION', 'rotFK', 'rig')\\n('rig:foot.l', 'COPY_SCALE', 'scaleFK', 'rig')\\n('rig:foot.l', 'COPY_LOCATION', 'locIK', 'rig')\\n('rig:toes_01.l', 'COPY_ROTATION', 'ik_rot', 'rig')\\n('rig:toes_01.l', 'COPY_SCALE', 'ik_scale', 'rig')\\n('rig:toes_01.l', 'COPY_ROTATION', 'fk_rot', 'rig')\\n('rig:toes_01.l', 'COPY_SCALE', 'fk_scale', 'rig')\\n('rig:c_foot_fk_scale_fix.l', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_fk_pre_pole.l', 'COPY_LOCATION', 'Copy Location', 'rig')\\n('rig:leg_fk_pre_pole.l', 'COPY_LOCATION', 'Copy Location.001', 'rig')\\n('rig:leg_fk_pre_pole.l', 'DAMPED_TRACK', 'Damped Track', 'rig')\\n('rig:thigh.l', 'COPY_ROTATION', 'rotIK', 'rig')\\n('rig:thigh.l', 'COPY_SCALE', 'scaleIK', 'rig')\\n('rig:thigh.l', 'COPY_ROTATION', 'rotFK', 'rig')\\n('rig:thigh.l', 'COPY_SCALE', 'scaleFK', 'rig')\\n('rig:c_stretch_leg.l', 'COPY_LOCATION', 'Copy Location', 'rig')\\n('rig:leg_pole_line.l', 'STRETCH_TO', 'Stretch To_IKline', 'rig')\\n('rig:leg.l', 'COPY_ROTATION', 'rotIK', 'rig')\\n('rig:leg.l', 'COPY_LOCATION', 'locIK', 'rig')\\n('rig:leg.l', 'STRETCH_TO', 'Stretch To', 'rig')\\n('rig:leg.l', 'COPY_ROTATION', 'rotFK', 'rig')\\n('rig:leg.l', 'COPY_LOCATION', 'locFK', 'rig')\\n('rig:leg.l', 'COPY_SCALE', 'scaleFK', 'rig')\\n('rig:thigh_twist.l', 'STRETCH_TO', 'Stretch To', 'rig')\\n('rig:leg_ik_nostr.l', 'IK', 'IK', 'rig')\\n('rig:thigh_ik.r', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:thigh_ik.r', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.r', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.r', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:leg_ik.r', 'IK', 'autostretch', 'rig')\\n('rig:leg_ik_nostr.r', 'IK', 'IK', 'rig')\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: --- Empty.001 parent None\\n Empty.001: Track To TRACK_TO target=Empty\\n--- Empty parent Empty.001\\n--- Power Broom parent None\\n--- Power Broom.001 parent None\\n Power Broom.001: Child_Of_Empty.001 CHILD_OF target=Empty.001\\nCONSTRAINTS_TO_RIG 741\\n('rig:root.x', 'COPY_TRANSFORMS', 'Copy Transforms_FWD', 'rig')\\n('rig:root.x', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:thigh_ik.l', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:thigh_ik.l', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.l', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.l', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:leg_ik.l', 'IK', 'autostretch', 'rig')\\n('rig:foot.l', 'COPY_ROTATION', 'rotIK', 'rig')\\n('rig:foot.l', 'COPY_SCALE', 'scaleIK', 'rig')\\n('rig:foot.l', 'COPY_ROTATION', 'rotFK', 'rig')\\n('rig:foot.l', 'COPY_SCALE', 'scaleFK', 'rig')\\n('rig:foot.l', 'COPY_LOCATION', 'locIK', 'rig')\\n('rig:toes_01.l', 'COPY_ROTATION', 'ik_rot', 'rig')\\n('rig:toes_01.l', 'COPY_SCALE', 'ik_scale', 'rig')\\n('rig:toes_01.l', 'COPY_ROTATION', 'fk_rot', 'rig')\\n('rig:toes_01.l', 'COPY_SCALE', 'fk_scale', 'rig')\\n('rig:c_foot_fk_scale_fix.l', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_fk_pre_pole.l', 'COPY_LOCATION', 'Copy Location', 'rig')\\n('rig:leg_fk_pre_pole.l', 'COPY_LOCATION', 'Copy Location.001', 'rig')\\n('rig:leg_fk_pre_pole.l', 'DAMPED_TRACK', 'Damped Track', 'rig')\\n('rig:thigh.l', 'COPY_ROTATION', 'rotIK', 'rig')\\n('rig:thigh.l', 'COPY_SCALE', 'scaleIK', 'rig')\\n('rig:thigh.l', 'COPY_ROTATION', 'rotFK', 'rig')\\n('rig:thigh.l', 'COPY_SCALE', 'scaleFK', 'rig')\\n('rig:c_stretch_leg.l', 'COPY_LOCATION', 'Copy Location', 'rig')\\n('rig:leg_pole_line.l', 'STRETCH_TO', 'Stretch To_IKline', 'rig')\\n('rig:leg.l', 'COPY_ROTATION', 'rotIK', 'rig')\\n('rig:leg.l', 'COPY_LOCATION', 'locIK', 'rig')\\n('rig:leg.l', 'STRETCH_TO', 'Stretch To', 'rig')\\n('rig:leg.l', 'COPY_ROTATION', 'rotFK', 'rig')\\n('rig:leg.l', 'COPY_LOCATION', 'locFK', 'rig')\\n('rig:leg.l', 'COPY_SCALE', 'scaleFK', 'rig')\\n('rig:thigh_twist.l', 'STRETCH_TO', 'Stretch To', 'rig')\\n('rig:leg_ik_nostr.l', 'IK', 'IK', 'rig')\\n('rig:thigh_ik.r', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:thigh_ik.r', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.r', 'COPY_SCALE', 'Copy Scale', 'rig')\\n('rig:leg_ik.r', 'COPY_ROTATION', 'Copy Rotation', 'rig')\\n('rig:leg_ik.r', 'IK', 'autostretch', 'rig')\\n('rig:leg_ik_nostr.r', 'IK', 'IK', 'rig')\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Analyzing hierarchy** + +The rig hierarchy shows Empty parented to Empty.001, which tracks Empty, and Power Broom.001 uses CHILD_OF to reference Empty.001. I found that retargeting only swaps direct parent to orig and constraint.target == orig, not the full chain. I'm checking if Empty is in orig_hierarchy by walking the parent chain to see if it qualifies as a descendant. I found that Empty qualifies as a descendant of rig if Empty.parent == rig. I discovered that props parented directly to the rig (bone parenting) are excluded from parent reparenting because they're direct children of the armature in Blender. + +**Retargeting constraints** + +I'm updating constraints that target the original object to point to the replacement object instead. I found that RetargRelatives doesn't walk indirect chains and only processes direct parents outside of orig's hierarchy. I'm identifying candidates for retargeting by including descendants, direct children, and objects with parent==orig, then removing orig hierarchy. I realized that objects parented directly to the armature aren't being retargeted because they're excluded in the first loop due to their parent relationship. The armature's descendant status means it's removed from candidates, so the first loop only processes external objects. I found that all mesh children of rig are excluded from retargeting because they're in orig_hierarchy. + +Props parented to rig are intentionally excluded from parent swap logic. I'm adjusting parent swap logic to handle constraint-only props that aren't in orig_hierarchy.
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\ndef dump(ob):\\n print(\\\"====\\\", ob.name, \\\"parent=\\\", ob.parent.name if ob.parent else None, \\\"pb=\\\", repr(ob.parent_bone) if ob.parent else \\\"\\\")\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n print(f\\\" {c.name}: {c.type} -\u003e {t.name if t else None} sub={getattr(c, 'subtarget', '')!r}\\\")\\n\\n# resolve two rigs\\nrigs = [o for o in bpy.data.objects if o.type == \\\"ARMATURE\\\"]\\nfor r in rigs:\\n print(\\\"RIG\\\", repr(r.name), \\\"id(r)\\\", r.as_pointer(), \\\"lib\\\", r.library.filepath if r.library else \\\"LOCAL\\\")\\n\\nfor nm in [\\\"Empty\\\", \\\"Empty.001\\\"]:\\n o = bpy.data.objects.get(nm)\\n if o:\\n dump(o)\\n\\n# Modifiers on brooms\\nfor nm in [\\\"Power Broom\\\", \\\"Power Broom.001\\\"]:\\n o = bpy.data.objects.get(nm)\\n if o and o.type == \\\"MESH\\\":\\n for m in o.modifiers:\\n mo = getattr(m, \\\"object\\\", None)\\n print(nm, \\\"mod\\\", m.type, \\\"obj\\\", mo.name if mo else None)\\n\",\"user_prompt\":\"Dump Empty chain and broom armature modifiers\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: RIG 'rig' id(r) 2780156999968 lib LOCAL\\nRIG 'rig.001' id(r) 2780221642272 lib LOCAL\\nRIG 'rig' id(r) 2782476805152 lib //..\\\\..\\\\..\\\\Assets\\\\Blends\\\\Char\\\\TF-rig_2.03.blend\\n==== Empty parent= Empty.001 pb= ''\\n==== Empty.001 parent= None pb= \\n Track To: TRACK_TO -\u003e Empty sub=''\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: RIG 'rig' id(r) 2780156999968 lib LOCAL\\nRIG 'rig.001' id(r) 2780221642272 lib LOCAL\\nRIG 'rig' id(r) 2782476805152 lib //..\\\\..\\\\..\\\\Assets\\\\Blends\\\\Char\\\\TF-rig_2.03.blend\\n==== Empty parent= Empty.001 pb= ''\\n==== Empty.001 parent= None pb= \\n Track To: TRACK_TO -\u003e Empty sub=''\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\n# Get local rigs only (two pointers for 'rig' name)\\nlocal_rigs = []\\nfor o in bpy.data.objects:\\n if o.type == \\\"ARMATURE\\\" and o.name == \\\"rig\\\" and not o.library:\\n local_rigs.append(o)\\nprint(\\\"LOCAL rig objects:\\\", [(o.as_pointer(), o.name) for o in local_rigs])\\n\\nrig_local = local_rigs[0] if local_rigs else None\\nrig001 = bpy.data.objects.get(\\\"rig.001\\\")\\n\\ndef children_of(arm):\\n ch = []\\n for ob in bpy.data.objects:\\n if ob.parent and ob.parent == arm:\\n ch.append(ob)\\n return ch\\n\\nif rig_local:\\n print(\\\"Direct children of rig (local):\\\", [o.name for o in children_of(rig_local)][:30])\\nif rig001:\\n print(\\\"Direct children of rig.001:\\\", [o.name for o in children_of(rig001)][:30])\\n\\n# Anything parented to rig (any rig named rig)\\nfor o in bpy.data.objects:\\n if o.parent and o.parent.type == \\\"ARMATURE\\\" and \\\"rig\\\" in o.parent.name:\\n if \\\"Empty\\\" in o.name or \\\"Broom\\\" in o.name or \\\"broom\\\" in o.name.lower():\\n print(\\\"RIG_PARENT\\\", o.name, \\\"-\u003e\\\", o.parent.name, \\\"bone\\\", o.parent_bone)\\n\\n# Modifiers referencing rig armatures\\nfor ob in bpy.data.objects:\\n if ob.type != \\\"MESH\\\":\\n continue\\n for m in ob.modifiers:\\n mo = getattr(m, \\\"object\\\", None)\\n if mo and mo.type == \\\"ARMATURE\\\" and \\\"rig\\\" in mo.name:\\n if \\\"Broom\\\" in ob.name or \\\"Empty\\\" in ob.name:\\n print(\\\"MOD\\\", ob.name, m.type, mo.name)\\n\",\"user_prompt\":\"Find rig parenting for Empty/Broom objects\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: LOCAL rig objects: [(2780156999968, 'rig')]\\nDirect children of rig (local): ['Sunglasses', 'TF_arms', 'TF_body', 'TF_legs', 'Turf_Hat']\\nDirect children of rig.001: ['Sunglasses.001', 'TF_arms.001', 'TF_body.001', 'TF_legs.001', 'Turf_Hat.001']\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: LOCAL rig objects: [(2780156999968, 'rig')]\\nDirect children of rig (local): ['Sunglasses', 'TF_arms', 'TF_body', 'TF_legs', 'Turf_Hat']\\nDirect children of rig.001: ['Sunglasses.001', 'TF_arms.001', 'TF_body.001', 'TF_legs.001', 'Turf_Hat.001']\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\ndef drivers_on(ob):\\n if not ob.animation_data or not ob.animation_data.drivers:\\n return []\\n return [d for d in ob.animation_data.drivers]\\n\\nfor nm in [\\\"Empty.001\\\", \\\"Empty\\\", \\\"Power Broom.001\\\", \\\"Power Broom\\\"]:\\n ob = bpy.data.objects.get(nm)\\n if not ob:\\n continue\\n drs = drivers_on(ob)\\n print(nm, \\\"driver_count\\\", len(drs))\\n for d in drs[:8]:\\n print(\\\" \\\", d.data_path, d.driver.expression[:80] if d.driver else \\\"\\\")\\n\\n# Also shape keys / data level\\nfor nm in [\\\"Power Broom.001\\\"]:\\n ob = bpy.data.objects.get(nm)\\n if ob and ob.data and getattr(ob.data, \\\"animation_data\\\", None):\\n drs = ob.data.animation_data.drivers\\n print(nm, \\\"data drivers\\\", len(drs))\\n\",\"user_prompt\":\"Check drivers on Empty and Power Broom\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: Empty.001 driver_count 0\\nEmpty driver_count 0\\nPower Broom.001 driver_count 0\\nPower Broom driver_count 0\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: Empty.001 driver_count 0\\nEmpty driver_count 0\\nPower Broom.001 driver_count 0\\nPower Broom driver_count 0\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\nrig = bpy.data.objects.get(\\\"rig\\\")\\nrig001 = bpy.data.objects.get(\\\"rig.001\\\")\\n\\ndef inspect_child(name):\\n ob = bpy.data.objects.get(name)\\n if not ob:\\n print(\\\"missing\\\", name)\\n return\\n print(\\\"---\\\", name, \\\"parent\\\", ob.parent.name if ob.parent else None, ob.parent_bone)\\n for m in ob.modifiers:\\n mo = getattr(m, \\\"object\\\", None)\\n if m.type == \\\"ARMATURE\\\" or mo:\\n print(\\\" mod\\\", m.name, m.type, \\\"-\u003e\\\", mo.name if mo else None)\\n\\nfor nm in [\\\"Turf_Hat\\\", \\\"Turf_Hat.001\\\", \\\"Sunglasses\\\", \\\"Sunglasses.001\\\", \\\"TF_body\\\", \\\"TF_body.001\\\"]:\\n inspect_child(nm)\\n\\n# Simulate descendants() and candidates logic for orig=rig, rep=rig.001\\nfrom collections import defaultdict\\n\\ndef descendants(armature):\\n out = set()\\n for ob in bpy.data.objects:\\n p = ob.parent\\n while p:\\n if p == armature:\\n out.add(ob)\\n break\\n p = p.parent\\n return out\\n\\nif rig and rig001:\\n oh = {rig} | descendants(rig)\\n print(\\\"orig_hierarchy count\\\", len(oh))\\n # sample: is Turf_Hat in oh?\\n for nm in [\\\"Turf_Hat\\\", \\\"Sunglasses\\\", \\\"Power Broom\\\"]:\\n o = bpy.data.objects.get(nm)\\n if o:\\n print(nm, \\\"in orig_hierarchy\\\", o in oh)\\n\",\"user_prompt\":\"Inspect rig children meshes and descendants() membership\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: --- Turf_Hat parent rig c_head.x\\n mod Armature ARMATURE -\u003e None\\n--- Turf_Hat.001 parent rig.001 c_head.x\\n mod Armature ARMATURE -\u003e None\\n--- Sunglasses parent rig c_head.x\\n--- Sunglasses.001 parent rig.001 c_head.x\\n--- TF_body parent rig \\n mod rig ARMATURE -\u003e rig\\n--- TF_body.001 parent rig.001 \\n mod rig ARMATURE -\u003e rig.001\\norig_hierarchy count 6\\nTurf_Hat in orig_hierarchy True\\nSunglasses in orig_hierarchy True\\nPower Broom in orig_hierarchy False\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: --- Turf_Hat parent rig c_head.x\\n mod Armature ARMATURE -\u003e None\\n--- Turf_Hat.001 parent rig.001 c_head.x\\n mod Armature ARMATURE -\u003e None\\n--- Sunglasses parent rig c_head.x\\n--- Sunglasses.001 parent rig.001 c_head.x\\n--- TF_body parent rig \\n mod rig ARMATURE -\u003e rig\\n--- TF_body.001 parent rig.001 \\n mod rig ARMATURE -\u003e rig.001\\norig_hierarchy count 6\\nTurf_Hat in orig_hierarchy True\\nSunglasses in orig_hierarchy True\\nPower Broom in orig_hierarchy False\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Analyzing modifier data** + +The armature modifier's object points to the rig, and the modifier name is 'rig', not the mesh name. I identified that the rig contains 5 character meshes plus 1 original object, totaling 6 objects.
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\n# List ALL constraints on empties and brooms that reference armatures\\nfor ob in bpy.data.objects:\\n if ob.type not in (\\\"EMPTY\\\", \\\"MESH\\\"):\\n continue\\n if \\\"Broom\\\" not in ob.name and \\\"Empty\\\" not in ob.name:\\n continue\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n if t and t.type == \\\"ARMATURE\\\":\\n print(ob.name, c.type, \\\"-\u003e\\\", t.name, getattr(c, \\\"subtarget\\\", \\\"\\\"))\\n\\n# Search any object with constraint to local rig\\nrig = bpy.data.objects.get(\\\"rig\\\")\\nif rig:\\n ext = []\\n for ob in bpy.data.objects:\\n if ob in ({rig} | __import__(\\\"sys\\\").modules.get(\\\"bpy\\\")):\\n pass\\n for ob in bpy.data.objects:\\n for c in ob.constraints:\\n if getattr(c, \\\"target\\\", None) == rig:\\n ext.append((ob.name, c.name, c.type))\\n print(\\\"OBJ constraints targeting LOCAL rig:\\\", len(ext))\\n for e in ext[:50]:\\n print(e)\\n\",\"user_prompt\":\"Find constraints from Empty/Broom to armature rig\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Error executing code: Communication error with Blender: Code execution error: unsupported operand type(s) for |: 'set' and 'module'\"}],\"structuredContent\":{\"result\":\"Error executing code: Communication error with Blender: Code execution error: unsupported operand type(s) for |: 'set' and 'module'\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\nrig = bpy.data.objects.get(\\\"rig\\\")\\nrig001 = bpy.data.objects.get(\\\"rig.001\\\")\\n\\nfor ob in bpy.data.objects:\\n if ob.type not in (\\\"EMPTY\\\", \\\"MESH\\\"):\\n continue\\n if \\\"Broom\\\" not in ob.name and \\\"Empty\\\" not in ob.name:\\n continue\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n if t and getattr(t, \\\"type\\\", None) == \\\"ARMATURE\\\":\\n print(ob.name, c.type, \\\"-\u003e\\\", t.name, repr(getattr(c, \\\"subtarget\\\", \\\"\\\")))\\n\\nif rig:\\n ext = []\\n for ob in bpy.data.objects:\\n for c in ob.constraints:\\n if getattr(c, \\\"target\\\", None) == rig:\\n ext.append((ob.name, c.name, c.type))\\n print(\\\"OBJ constraints targeting LOCAL rig:\\\", len(ext))\\n for e in ext[:40]:\\n print(e)\\n\",\"user_prompt\":\"Constraints to local rig from empties/brooms\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: OBJ constraints targeting LOCAL rig: 0\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: OBJ constraints targeting LOCAL rig: 0\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Analyzing retargeting needs** + +I need to identify which object constraints and camera DOF settings require retargeting during rig migration. The core migration issue is that meshes parented to rig bones are excluded from retargeting, requiring manual hierarchy fixes. I found that RetargRelatives doesn't process objects without rig chain connections, only those with CHILD_OF constraints.
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
Tool use: **read_file_v2** • Read file: T:/Dynamic-Link-Manager/ops/migrator.py + +
+
+ +
+Tool use: **read_file_v2** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Checking constraint retargeting** + +I'm investigating why armature modifiers on TF_body aren't being updated during retargeting despite being inside the rig hierarchy. I need to ensure armature modifiers point to the correct rig during retargeting by duplicating objects with matching rig names. I need to verify that armature modifiers point to the correct rig during retargeting by duplicating objects with matching rig names. + +**Retargeting armature modifiers** + +I'll enhance retargeting to update armature modifiers on all meshes globally, not just candidates, to fix issues with duplicate rigs having incorrect modifiers. I'll add optional global armature modifier retargeting in a third pass to fix orphan meshes with incorrect modifiers. I'm adding a second loop to handle armature modifier retargeting for local workflows while preserving linked rig internals. I'm updating linked rig modifier objects to point to local overrides while preserving internal rig references.
+ +_**Agent (composer-2-fast)**_ + + +Checking for other references to `orig` (drivers, geometry nodes) and verifying the modifier gap. + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\nrig = bpy.data.objects.get(\\\"rig\\\")\\nrig001 = bpy.data.objects.get(\\\"rig.001\\\")\\n# Meshes with armature mod still pointing to rig while parent is rig001 (mismatch)\\nbad = []\\nfor ob in bpy.data.objects:\\n if ob.type != \\\"MESH\\\":\\n continue\\n for m in ob.modifiers:\\n mo = getattr(m, \\\"object\\\", None)\\n if mo == rig and ob.parent == rig001:\\n bad.append((ob.name, m.name))\\nprint(\\\"MISMATCH: parent rig.001 but modifier still rig:\\\", bad)\\n\\n# Meshes parented to rig with modifier object rig (expected before retarg)\\nstuck = []\\nfor ob in bpy.data.objects:\\n if ob.type != \\\"MESH\\\" or ob.parent != rig:\\n continue\\n for m in ob.modifiers:\\n if getattr(m, \\\"object\\\", None) == rig:\\n stuck.append(ob.name)\\nprint(\\\"Children of rig with armature mod to rig:\\\", stuck)\\n\",\"user_prompt\":\"Detect armature modifier / parent mismatches\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: MISMATCH: parent rig.001 but modifier still rig: []\\nChildren of rig with armature mod to rig: ['TF_arms', 'TF_body', 'TF_legs']\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: MISMATCH: parent rig.001 but modifier still rig: []\\nChildren of rig with armature mod to rig: ['TF_arms', 'TF_body', 'TF_legs']\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Migrating rig relationships** + +The rig relationship migration failed because Turf_Hat parent bone wasn't in the candidates list, so it wasn't reparented to rig.001. I'm adjusting the migration logic to ensure all meshes parented to 'rig' are reparented to 'rig.001' during character replacement. The RetargRelatives system intentionally preserves children of 'rig' by not moving them to 'rig.001' to avoid breaking linked assets. I'll summarize the retargeting approach for the user without implementing code changes unless requested. + +**Retargeting armature** + +I'll add a global armature modifier update for descendants to handle cases where meshes duplicate with .001 parent rig.001 but modifier forgot. I found root causes of retargeting failures: indirect constraint chains, excluded objects, and missing object constraints to rig. Rig constraints targeting rig itself are preserved during retargeting, but internal rig constraints point to old rig reference. + +Internal rig constraints must reference same armature or IK targets to preserve animation integrity. I found that self-referencing bone constraints on the original rig would incorrectly retarget to rig.001 instead of preserving self-reference. + +I'll skip bone constraints where target equals original to prevent incorrect self-referencing. I'm skipping bone constraints where target equals original to prevent incorrect self-referencing. + +I found that rig-to-rig bone constraints may need special handling to avoid incorrect target changes during migration. I'm investigating why rig-to-rig IK constraints are incorrectly targeting `rig.001` instead of the original rig during migration. **Investigating RetargRelatives** + +The RetargRelatives issue stems from Power Broom's chain lacking direct targeting of 'rig' object, causing the constraint system to fail. RetargRelatives only handles exact target matches and doesn't traverse indirect chains, causing parented objects to be removed from candidates. I need to handle cases where orig='rig' and rep='rig.001' to prevent incorrect bone constraint targeting. I need to ensure pose bones on the old rig target the new rig's bones correctly during migration to avoid breaking the rig. + +**Retargeting relatives** + +I'm ensuring constraints target rig.001 after bone_const copies them, since run_retarg_relatives runs after run_mig_bone_const and retargets all objects including changing rig targets. I'm adjusting the constraint targeting to ensure Power Broom connects directly to rig armature bone for visibility.
+ +_**Agent (composer-2-fast)**_ + + +Here is what the Blender MCP pass showed and how it lines up with the add-on. + +### What the scene is doing + +- **`Power Broom.001`** only has **CHILD_OF → `Empty.001`**. It does **not** reference the armature. +- **`Empty.001`** only has **TRACK_TO → `Empty`**. **`Empty`** is parented to **`Empty.001`** (small hierarchy between empties only). +- There are **no object-level constraints** on those empties/brooms whose **`target` is your local `rig`** (the tool reported **0** such constraints). + +So **RetargRelatives** has **nothing** to swap for the broom chain: it never sees `orig` (`rig`) as a `constraint.target`, only **`Empty` / `Empty.001`**. + +### How `RetargRelatives` actually works + +From `run_retarg_relatives` in `ops/migrator.py`: + +1. It **reparents** and **armature-modifier** updates only for objects in **`candidates`**, and **`candidates` excludes `orig_hierarchy`** (the armature plus every object whose **parent chain** reaches the original armature). So **children of `rig`** are **not** moved to `rig.001` by that path (by design, for linked-rig safety). +2. It **retargets** `constraint.target` **only where `target == orig`** (same for pose-bone constraints and camera DOF). +3. It does **not** walk **indirect** chains (e.g. broom → empty → … → rig). If the rig never appears as **`target`**, the operator cannot retarget it. + +Changing **parenting** to a **constraint** only helps if that constraint’s **`target`** is the **original armature** (`rig`). A chain that only references **empties** or **other bones** is invisible to this step. + +### What to do for the power broom + +- Add **`Child Of` / `Copy Transforms` / `Damped Track`** (or bone parenting) **from the empty or the broom** so **`target` = `rig`** and a **subtarget** (bone) as needed. +- Run **RetargRelatives** again with **Original = `rig`**, **Replacement = `rig.001`** so that **`rig` → `rig.001`** on that constraint. + +Or set those targets to **`rig.001`** by hand. + +### If the real problem is body/clothes **on** `rig` + +Those are **under** `orig_hierarchy`, so the **first** block does **not** reparent them or fix their **Armature** modifiers; only the **constraint**/`target` passes run **globally**. If you still see wrong **Armature** modifiers on meshes parented to **`rig.001`**, say so and we can add a **separate** pass that updates **all** Armature modifiers with `object == orig` (not only `candidates`)—that’s a small, focused code change. + +--- + +_**User**_ + +@c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt is there a dependency cycle that can be resolved in blend, or is that in the character rig blend? + +--- + +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\projects\t-Dynamic-Link-Manager\terminals\3.txt + +
+
+ +
+Tool use: **read_file_v2** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Identifying dependency cycle** + +I found the dependency cycle is in the shot file setup involving Track To and Parent constraints. The dependency cycle involves Track To and Child Of constraints between Empty.001 and Empty objects.
+ +_**Agent (composer-2-fast)**_ + + +The log in `3.txt` spells it out: **this dependency cycle is in your shot file**, not in the linked character rig (`TF-rig_2.03.blend`). + +Blender reports: + +```text +OBEmpty.001 … depends on OBEmpty … via 'Track To' +OBEmpty … depends on OBEmpty.001 … via 'Child Of' / parenting +``` + +So **`Empty.001`** is driven by **Track To → `Empty`**, while **`Empty`** is **under `Empty.001`** in the hierarchy (your outliner parent chain). That closes a loop: parent evaluation needs the child, and the child’s constraint needs the parent. + +**How to fix it (in this blend):** break one link in the loop, for example: + +- **Remove** the **parent** between the two empties and drive both from something else (e.g. both constrained to the **rig** / bone, or one fixed empty + one constraint), **or** +- **Remove** the **Track To** and use a non-cyclic setup (e.g. single parent + **Copy Rotation** to a neutral target, or parent only to the armature bone). + +So: **resolve it in the scene blend** by changing the Empty / Empty.001 rigging; you don’t need to open the character rig `.blend` for this cycle. + +--- + +Separate lines in the log: **`simple_renaming_panel`** is erroring on Blender 5 (`Action` has no `fcurves` — API/slots change). That’s an **addon bug**, unrelated to the depsgraph cycle above. Disable or update that addon if the spam bothers you. + +--- + +_**User**_ + +explain the chain to me. I just converted to constraints, so scan the file again. + +--- + +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\ndef dump_ob(ob, indent=0):\\n pad = \\\" \\\" * indent\\n print(f\\\"{pad}{ob.name} ({ob.type})\\\")\\n if ob.parent:\\n print(f\\\"{pad} parent: {ob.parent.name} bone={ob.parent_bone!r} type={ob.parent_type}\\\")\\n else:\\n print(f\\\"{pad} parent: None\\\")\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n subt = getattr(c, \\\"subtarget\\\", \\\"\\\") or \\\"\\\"\\n inf = getattr(c, \\\"influence\\\", 1.0)\\n print(f\\\"{pad} constraint: {c.name!r} [{c.type}] influence={inf}\\\")\\n if t:\\n print(f\\\"{pad} target={t.name} ({t.type}) subtarget={subt!r}\\\")\\n if c.type == \\\"CHILD_OF\\\":\\n print(f\\\"{pad} use_location={c.use_location} use_rotation={c.use_rotation} use_scale={c.use_scale}\\\")\\n\\n# Focus on empties and brooms\\nnames = [o.name for o in bpy.data.objects if \\\"Empty\\\" in o.name or \\\"Broom\\\" in o.name or \\\"broom\\\" in o.name.lower()]\\nnames = sorted(set(names))\\nprint(\\\"OBJECTS:\\\", names)\\nfor nm in names:\\n ob = bpy.data.objects.get(nm)\\n if ob:\\n dump_ob(ob)\\n print()\\n\",\"user_prompt\":\"Scan Empty/Broom parent and constraint chain after user edits\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Error executing code: Communication error with Blender: Code execution error: 'ChildOfConstraint' object has no attribute 'use_location'\"}],\"structuredContent\":{\"result\":\"Error executing code: Communication error with Blender: Code execution error: 'ChildOfConstraint' object has no attribute 'use_location'\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + + + + + + +_**Agent (composer-2-fast)**_ + + +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + + +Parameters: + +```json +{ + "tools": [ + { + "name": "execute_blender_code", + "parameters": "{\"code\":\"\\nimport bpy\\n\\ndef dump_ob(ob):\\n print(f\\\"== {ob.name} ({ob.type})\\\")\\n if ob.parent:\\n print(f\\\" parent: {ob.parent.name} bone={ob.parent_bone!r} parent_type={ob.parent_type}\\\")\\n else:\\n print(\\\" parent: None\\\")\\n if not ob.constraints:\\n print(\\\" (no object constraints)\\\")\\n for c in ob.constraints:\\n t = getattr(c, \\\"target\\\", None)\\n subt = (getattr(c, \\\"subtarget\\\", None) or \\\"\\\") or \\\"\\\"\\n inf = getattr(c, \\\"influence\\\", 1.0)\\n extra = \\\"\\\"\\n if c.type == \\\"CHILD_OF\\\":\\n extra = f\\\" set_inverse={getattr(c, 'has_influence', '')}\\\"\\n print(f\\\" [{c.name}] {c.type} inf={inf}\\\")\\n if t:\\n print(f\\\" → target={t.name} ({t.type}) subtarget={subt!r}\\\")\\n else:\\n print(f\\\" → target=None\\\")\\n\\nnames = sorted({o.name for o in bpy.data.objects if \\\"Empty\\\" in o.name or \\\"Broom\\\" in o.name or \\\"broom\\\" in o.name.lower()})\\nprint(\\\"Matched:\\\", names)\\nfor nm in names:\\n ob = bpy.data.objects.get(nm)\\n if ob:\\n dump_ob(ob)\\n print()\\n\",\"user_prompt\":\"Scan Empty/Broom chain without ChildOf use_location\"}", + "serverName": "blender" + } + ] +} +``` +Additional data: + +```json +{ + "status": "success" +} +``` +Result: + +```json +{ + "result": "{\"content\":[{\"type\":\"text\",\"text\":\"Code executed successfully: Matched: ['Broom.Hair.001', 'Empty', 'Empty.001', 'Power Broom', 'Power Broom.001', 'Push Broom', 'Push Broom.001']\\n== Broom.Hair.001 (MESH)\\n parent: Push Broom.001 bone='' parent_type=OBJECT\\n (no object constraints)\\n\\n== Empty (EMPTY)\\n parent: None\\n [Child_Of_Empty.001] CHILD_OF inf=1.0\\n → target=Empty.001 (EMPTY) subtarget=''\\n\\n== Empty.001 (EMPTY)\\n parent: None\\n [Track To] TRACK_TO inf=0.0\\n → target=Empty (EMPTY) subtarget=''\\n\\n== Power Broom (MESH)\\n parent: None\\n (no object constraints)\\n\\n== Power Broom.001 (MESH)\\n parent: None\\n [Child_Of_Empty.001] CHILD_OF inf=1.0\\n → target=Empty.001 (EMPTY) subtarget=''\\n\\n== Push Broom (MESH)\\n parent: None\\n (no object constraints)\\n\\n== Push Broom.001 (MESH)\\n parent: None\\n (no object constraints)\\n\\n\"}],\"structuredContent\":{\"result\":\"Code executed successfully: Matched: ['Broom.Hair.001', 'Empty', 'Empty.001', 'Power Broom', 'Power Broom.001', 'Push Broom', 'Push Broom.001']\\n== Broom.Hair.001 (MESH)\\n parent: Push Broom.001 bone='' parent_type=OBJECT\\n (no object constraints)\\n\\n== Empty (EMPTY)\\n parent: None\\n [Child_Of_Empty.001] CHILD_OF inf=1.0\\n → target=Empty.001 (EMPTY) subtarget=''\\n\\n== Empty.001 (EMPTY)\\n parent: None\\n [Track To] TRACK_TO inf=0.0\\n → target=Empty (EMPTY) subtarget=''\\n\\n== Power Broom (MESH)\\n parent: None\\n (no object constraints)\\n\\n== Power Broom.001 (MESH)\\n parent: None\\n [Child_Of_Empty.001] CHILD_OF inf=1.0\\n → target=Empty.001 (EMPTY) subtarget=''\\n\\n== Push Broom (MESH)\\n parent: None\\n (no object constraints)\\n\\n== Push Broom.001 (MESH)\\n parent: None\\n (no object constraints)\\n\\n\"}}" +} +``` +Status: **completed** + + +
+
+ +
+Tool use: **mcp-blender-user-blender-execute_blender_code** + +
+ +_**Agent (composer-2-fast)**_ + +
Thought Process +**Breaking cycle** + +The cycle is broken because the Track To constraint has 0 influence, making it effectively disabled.
+ +_**Agent (composer-2-fast)**_ + + +Here’s what the file looks like **after** your constraint pass (fresh scan): + +### The chain + +1. **`Empty.001`** (empty, **no parent**) + - **`Track To` → `Empty`**, but **`influence = 0.0`** → this constraint does **nothing** right now (it’s effectively off). + +2. **`Empty`** (empty, **no parent**) + - **`Child Of` → `Empty.001`** (`Child_Of_Empty.001`), **`influence = 1.0`** → **`Empty`** follows **`Empty.001`**. + +3. **`Power Broom.001`** (mesh, **no parent**) + - **`Child Of` → `Empty.001`** (same idea as `Empty`) → broom mesh follows **`Empty.001`**. + +4. **`Broom.Hair.001`** + - **`Parent` → `Push Broom.001`** (object parenting), no object constraints listed. + +5. **`Power Broom`**, **`Push Broom`**, **`Push Broom.001`** + - **No parent**, **no object constraints** in this dump. + +So the **active** logic is: **`Empty.001` is the hub** → both **`Empty`** and **`Power Broom.001`** are **`Child Of` → `Empty.001`**. +The old cyclic bit was **parent + Track To**; you’ve removed the parenting and left a **Track To with 0 influence**, which **breaks the dependency cycle** in practice. + +### What to do next (if the broom should follow the character) + +Right now **nothing** in this chain points at **`rig` / `rig.001`** or a bone. **`Empty.001`** only moves when you move it (or animate it). To attach to the rig, you’d add a constraint **on `Empty.001`** (or on a single root empty) to the armature/bone, then **RetargRelatives** can swap **`rig` → `rig.001`** on that constraint **if** `target` is the original armature. + +If you want, we can scan again for **any** constraint from these empties to an armature after you hook **`Empty.001`** to the rig. diff --git a/.specstory/statistics.json b/.specstory/statistics.json index 537f8cd..11d261f 100644 --- a/.specstory/statistics.json +++ b/.specstory/statistics.json @@ -7,7 +7,7 @@ "end_timestamp": "2026-02-18T16:39:18-07:00", "markdown_size_bytes": 789326, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "14305e53-b999-4885-8511-924b3957eb04": { "user_message_count": 1, @@ -16,7 +16,7 @@ "end_timestamp": "2026-03-13T14:21:38-06:00", "markdown_size_bytes": 659, "provider": "cursoride", - "last_updated": "2026-03-25T20:24:24Z" + "last_updated": "2026-03-31T22:51:51Z" }, "15ca8ebd-f444-47c7-8df3-efe4c9e8386f": { "user_message_count": 1, @@ -25,7 +25,7 @@ "end_timestamp": "2026-03-10T10:07:27-06:00", "markdown_size_bytes": 9952, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "315fde62-50dd-4c50-9d07-4089c4952f9d": { "user_message_count": 1, @@ -34,7 +34,7 @@ "end_timestamp": "2026-03-13T14:28:15-06:00", "markdown_size_bytes": 1169, "provider": "cursoride", - "last_updated": "2026-03-25T20:24:24Z" + "last_updated": "2026-03-31T22:51:51Z" }, "3178f110-1a60-43b6-9d92-e189b2342c51": { "user_message_count": 1, @@ -43,7 +43,7 @@ "end_timestamp": "2026-03-13T14:27:44-06:00", "markdown_size_bytes": 880, "provider": "cursoride", - "last_updated": "2026-03-25T20:24:24Z" + "last_updated": "2026-03-31T22:51:51Z" }, "3d77131c-052c-4be7-b2e4-2326abcd6957": { "user_message_count": 1, @@ -52,7 +52,7 @@ "end_timestamp": "2026-03-10T10:15:27-06:00", "markdown_size_bytes": 8570, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "3f042eb5-f457-4e06-9b56-ee14ac7afd90": { "user_message_count": 1, @@ -61,7 +61,7 @@ "end_timestamp": "2026-02-19T11:53:38-07:00", "markdown_size_bytes": 36111, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "49972a6d-d81d-4647-b6b5-9c92f8599474": { "user_message_count": 1, @@ -70,7 +70,7 @@ "end_timestamp": "2026-03-10T10:09:58-06:00", "markdown_size_bytes": 12876, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "4ce1e38a-1e85-411b-93ee-a378eef56146": { "user_message_count": 91, @@ -79,7 +79,7 @@ "end_timestamp": "2025-08-20T16:43:14-06:00", "markdown_size_bytes": 784540, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "4f67a3b1-1537-4680-9763-d0c401c58d7e": { "user_message_count": 1, @@ -88,7 +88,7 @@ "end_timestamp": "2026-03-10T10:19:09-06:00", "markdown_size_bytes": 71838, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "577872ce-9067-4be5-bdb0-89d48d9424ca": { "user_message_count": 1, @@ -97,7 +97,7 @@ "end_timestamp": "2026-03-10T10:25:51-06:00", "markdown_size_bytes": 75382, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "73bda206-5380-40cc-afb8-b5fc426fc0dc": { "user_message_count": 1, @@ -106,7 +106,7 @@ "end_timestamp": "2026-03-10T10:17:09-06:00", "markdown_size_bytes": 11226, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "788ea0b5-9ff6-440d-993d-a05234d95c5a": { "user_message_count": 1, @@ -115,7 +115,7 @@ "end_timestamp": "2026-03-10T10:22:17-06:00", "markdown_size_bytes": 69439, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "7ac38efd-6dd0-42da-9a01-f7d5062046e9": { "user_message_count": 1, @@ -124,7 +124,7 @@ "end_timestamp": "2026-03-10T10:23:09-06:00", "markdown_size_bytes": 61512, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "7d4946a5-6831-4e4c-90f7-8e9a96fa62d5": { "user_message_count": 1, @@ -133,7 +133,7 @@ "end_timestamp": "2026-03-10T11:03:21-06:00", "markdown_size_bytes": 107701, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "986c4b32-627a-4d70-9edb-fcaf30e33ee2": { "user_message_count": 1, @@ -142,7 +142,7 @@ "end_timestamp": "2026-03-10T10:04:31-06:00", "markdown_size_bytes": 9282, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "9d47ad7e-a8c1-4b72-b7cb-d27d2232bf50": { "user_message_count": 1, @@ -151,7 +151,7 @@ "end_timestamp": "2026-03-10T11:24:38-06:00", "markdown_size_bytes": 13504, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "9dca74c5-e8b6-4191-ae49-949e8c742e4d": { "user_message_count": 1, @@ -160,7 +160,7 @@ "end_timestamp": "2026-03-10T10:07:46-06:00", "markdown_size_bytes": 58095, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "a6a5c4fa-5957-4577-9323-13723784a540": { "user_message_count": 1, @@ -169,7 +169,7 @@ "end_timestamp": "2026-03-10T10:05:03-06:00", "markdown_size_bytes": 1116, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "b450a89e-cc97-4ef3-a775-fe26c81c7d0b": { "user_message_count": 1, @@ -178,7 +178,7 @@ "end_timestamp": "2026-03-10T10:09:41-06:00", "markdown_size_bytes": 3758, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "be47994a-f179-482a-9af2-4d208a01d324": { "user_message_count": 1, @@ -187,7 +187,7 @@ "end_timestamp": "2026-03-10T10:07:14-06:00", "markdown_size_bytes": 2904, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "cb944af7-6783-4830-acd6-af18544ff67e": { "user_message_count": 1, @@ -196,7 +196,7 @@ "end_timestamp": "2026-03-10T10:10:35-06:00", "markdown_size_bytes": 5188, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "d613f220-0255-4158-8a5d-4f93d50aa47b": { "user_message_count": 1, @@ -205,7 +205,7 @@ "end_timestamp": "2026-03-10T10:17:43-06:00", "markdown_size_bytes": 3856, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "daaa74ba-72d7-4686-a95b-ae6379d17ee9": { "user_message_count": 1, @@ -214,7 +214,7 @@ "end_timestamp": "2026-03-10T10:25:21-06:00", "markdown_size_bytes": 3302, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "dd526f99-b20d-401d-abac-ea6770d3a424": { "user_message_count": 1, @@ -223,16 +223,16 @@ "end_timestamp": "2026-03-10T11:25:10-06:00", "markdown_size_bytes": 24412, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" }, "f09112e7-3ddb-4e4e-b91a-418e38b881bf": { - "user_message_count": 23, - "agent_message_count": 357, + "user_message_count": 26, + "agent_message_count": 395, "start_timestamp": "2026-03-13T14:29:12-06:00", "end_timestamp": "2026-03-13T14:29:12-06:00", - "markdown_size_bytes": 306017, + "markdown_size_bytes": 486740, "provider": "cursoride", - "last_updated": "2026-03-25T21:04:24Z" + "last_updated": "2026-03-31T23:05:51Z" }, "f1bb25c5-e5d4-4aad-88a3-f562ab877040": { "user_message_count": 1, @@ -241,7 +241,7 @@ "end_timestamp": "2026-03-10T10:08:58-06:00", "markdown_size_bytes": 21001, "provider": "cursoride", - "last_updated": "2026-03-25T20:22:34Z" + "last_updated": "2026-03-31T22:50:00Z" } } }