From bc01755c1ecfebfb0df241e106ce94ef2845ee07 Mon Sep 17 00:00:00 2001 From: Nathan Date: Wed, 18 Feb 2026 17:32:03 -0700 Subject: [PATCH] per step migrator operators --- ...character-migrator-workflow-integration.md | 662 +++++++++++++++++- ui/operators.py | 177 ++++- ui/panels.py | 8 + 3 files changed, 838 insertions(+), 9 deletions(-) diff --git a/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md b/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md index c9ac639..11c127c 100644 --- a/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md +++ b/.specstory/history/2026-02-18_23-39Z-character-migrator-workflow-integration.md @@ -2930,7 +2930,667 @@ So I need to remove the property and change the UI so that when the section is e
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\panels.py -Status: **loading** +
+ + + +--- + +
Thought Process + +I need to fix the properties file - I may have broken the BoolProperty by merging. Let me check what the property block looks like now. +
+ +--- + + + + + +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\properties.py + +
+
+ +--- + +
Thought Process + +Good - the properties file is correct. linked_libraries_expanded was removed and linked_libraries_section_expanded is intact. Done. +
+ +--- + + +Summary of changes: + +**`ui/properties.py`** +- Removed the `linked_libraries_expanded` property. +- Left `linked_libraries_section_expanded` as-is. + +**`ui/panels.py`** +- When the Linked Libraries section is expanded, the library list, reload button, search paths, and selected library details are shown directly (no inner expand/collapse row or `linked_libraries_expanded` usage). + +Only the main section toggle `linked_libraries_section_expanded` remains; the inner “Linked Libraries” expand is gone. + +--- + +_**User**_ + +I'll figure out bpy.ops.dlm.run_character_migration() later. For now, I want every single step to be its own button, its own operator. bpy.ops.dlm.run_character_migration() should invoke every single operator that can also be invoked individually. + +--- + +_**Agent (model default, mode Agent)**_ + +
Thought Process + +The user wants to split the character migration into individual step operators, each with its own button. Then `run_character_migration` should simply invoke all of those operators in sequence (so it can be run as one action, but each step is also available individually). + +Let me check the current migrator steps and how run_full_migration works, so I can plan the operators and UI. +
+ +--- + + + + + +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\panels.py + +
+
+ +--- + +
Thought Process + +There are 6 steps in the migrator (step 7 is inside step 4). So I need: + +1. DLM_OT_migrator_step_1 - Copy attributes (location, rotation, scale) +2. DLM_OT_migrator_step_2 - Migrate NLA +3. DLM_OT_migrator_step_3 - Custom properties +4. DLM_OT_migrator_step_4 - Bone constraints (includes step 7 trim) +5. DLM_OT_migrator_step_5 - Retarget relations +6. DLM_OT_migrator_step_6 - Replacement base body + +Each operator will: +- Resolve the pair (manual or automatic from props) +- Call the corresponding run_step_N from ops.migrator +- Report success/failure + +For steps 4 and 5 they need orig_to_rep (dict) and rep_descendants. So each step operator needs to get (orig, rep) then build orig_to_rep = {orig: rep} and rep_descendants = descendants(rep) when needed. + +run_character_migration will then just execute the 6 operators in order via bpy.ops.dlm.migrator_step_1(), ... step_6(). We need to be careful: calling bpy.ops from Python passes context; the operators need to get the pair from context.scene.dynamic_link_manager each time. So run_character_migration can do: + + bpy.ops.dlm.migrator_step_1() + bpy.ops.dlm.migrator_step_2() + ... + (or we could keep calling the run_step_N functions directly from run_character_migration to avoid 6 operator invocations - but the user said "invoke every single operator". So they want run_character_migration to literally call the operators. So each operator is a wrapper that gets pair, calls run_step_N. And run_character_migration does for i in 1..6: bpy.ops.dlm.migrator_step_i(). We need to pass context - in Blender, when you call bpy.ops.x from another operator's execute(), you use context. So it would be context.ops.dlm.migrator_step_1() or we need to get the right context. Actually in Blender, from an operator's execute we do bpy.ops.dlm.migrator_step_1() - that uses the current context. So we're good. + +Plan: +1. Add 6 new operators in ui/operators.py: DLM_OT_migrator_step_1 through DLM_OT_migrator_step_6. Each gets (orig, rep) from context (manual or automatic), then calls the corresponding run_step_N from ops.migrator. Steps 4 and 5 need orig_to_rep and rep_descendants - they get that from orig, rep (orig_to_rep = {orig: rep}, rep_descendants = descendants(rep)). +2. Change DLM_OT_run_character_migration execute() to call bpy.ops.dlm.migrator_step_1() through bpy.ops.dlm.migrator_step_6() in sequence. If any returns CANCELLED, report and return CANCELLED. +3. In ui/panels.py, add 6 buttons for the 6 steps (and keep "Run migration" which runs all). + +Let me also check the migrator for run_step_5 and run_step_6 signatures. +
+ +--- + + + + + +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ops\migrator.py + +
+
+ +--- + +
Thought Process + +I have the full picture. Each step operator needs to resolve the pair once; steps 4, 5, 6 need orig_to_rep and/or rep_descendants. So either each operator resolves the pair (and steps 4/5/6 also compute orig_to_rep and rep_descendants), or we have a small helper that returns (orig, rep, orig_to_rep, rep_descendants) and each operator uses that. I'll have each operator get the pair via the same logic as run_full_migration (manual/auto from props), then steps 4 and 5 also compute orig_to_rep = {orig: rep} and rep_descendants = descendants(rep). + +Plan: +1. **ui/operators.py**: Add 6 operators (DLM_OT_migrator_step_1 … DLM_OT_migrator_step_6). Each in execute(): resolve (orig, rep) using migrator.get_pair_manual/automatic and props.migrator_mode; if no pair, report and CANCELLED. Then call the corresponding run_step_N. Steps 4 and 5 need orig_to_rep = {orig: rep} and rep_descendants = descendants(rep) from utils. Add these to OPERATOR_CLASSES. +2. **DLM_OT_run_character_migration**: In execute(), call in order: bpy.ops.dlm.migrator_step_1(), migrator_step_2(), ... migrator_step_6(). Use context. If any returns {'CANCELLED'}, report and return CANCELLED; else report success and FINISHED. +3. **ui/panels.py**: In the CharMig box, add a row with 6 buttons (Step 1: Copy attributes, Step 2: NLA, etc.) and keep the "Run migration" button (or put it first). Layout could be: Run migration (full); then Step 1, Step 2, ... Step 6 as individual buttons. + +Creating the plan. +
+ +--- + + +Creating a plan for per-step operators and wiring the full migration to invoke them. + + +--- + + +
+Tool use: **create_plan** + + +Parameters: + +```json +{ + "plan": "# Per-step character migrator operators\n\n## Goal\n\n- **Six step operators:** Step 1 (copy attributes), Step 2 (NLA), Step 3 (custom props), Step 4 (bone constraints + trim), Step 5 (retarget relations), Step 6 (base body override + shape-key action). Each is invokable on its own via a button.\n- **Run migration:** `bpy.ops.dlm.run_character_migration()` should invoke these six operators in order (no direct call to `run_full_migration` logic); it remains the “run all” action.\n\n## 1. New operators in [ui/operators.py](T:\\Dynamic-Link-Manager\\ui\\operators.py)\n\nAdd six operators, e.g. `DLM_OT_migrator_step_1` … `DLM_OT_migrator_step_6`, with `bl_idname` like `dlm.migrator_step_1` … `dlm.migrator_step_6`.\n\n**Shared behavior in each:**\n\n- In `execute(self, context)`:\n - Get pair from scene: `props = context.scene.dynamic_link_manager`, `use_auto = props.migrator_mode`, then `orig, rep = get_pair_automatic(context) if use_auto else get_pair_manual(context)` (using existing `get_pair_manual` / `get_pair_automatic` from [ops/migrator.py](T:\\Dynamic-Link-Manager\\ops\\migrator.py)).\n - If `not orig or not rep` or `orig == rep`: `self.report({'ERROR'}, ...)`, `return {'CANCELLED'}`.\n - Call the corresponding `run_step_N(...)` from `ops.migrator` (and for steps 4 and 5, also compute `orig_to_rep = {orig: rep}` and `rep_descendants = descendants(rep)` using [utils](T:\\Dynamic-Link-Manager\\utils\\__init__.py)).\n- Use `bl_options = {'REGISTER', 'UNDO'}` where appropriate (all steps modify data).\n- Keep logic in [ops/migrator.py](T:\\Dynamic-Link-Manager\\ops\\migrator.py) as-is; operators only resolve the pair and call `run_step_1` … `run_step_6`.\n\n**Step-specific calls:**\n\n| Operator | Calls |\n|---------------------|--------|\n| migrator_step_1 | `run_step_1(orig, rep)` |\n| migrator_step_2 | `run_step_2(orig, rep)` |\n| migrator_step_3 | `run_step_3(orig, rep)` |\n| migrator_step_4 | `orig_to_rep = {orig: rep}`, `run_step_4(orig, rep, orig_to_rep)` |\n| migrator_step_5 | `rep_descendants = descendants(rep)`, `orig_to_rep = {orig: rep}`, `run_step_5(orig, rep, rep_descendants, orig_to_rep)` |\n| migrator_step_6 | `rep_descendants = descendants(rep)`, `run_step_6(orig, rep, rep_descendants)` |\n\nWrap each `run_step_N` call in try/except; on exception report and return `{'CANCELLED'}`. On success report a short message and return `{'FINISHED'}`.\n\nAppend the six classes to `OPERATOR_CLASSES`.\n\n## 2. Change run_character_migration in [ui/operators.py](T:\\Dynamic-Link-Manager\\ui\\operators.py)\n\nIn `DLM_OT_run_character_migration.execute(self, context)`:\n\n- Do **not** call `run_full_migration(context)`.\n- Call in order: \n `bpy.ops.dlm.migrator_step_1()`, `bpy.ops.dlm.migrator_step_2()`, … `bpy.ops.dlm.migrator_step_6()`.\n- Use the same `context` (e.g. pass it if the Blender API requires it; typically `bpy.ops.dlm.migrator_step_1()` uses the current context).\n- If any of these returns `{'CANCELLED'}`, report which step failed and return `{'CANCELLED'}`.\n- If all return `{'FINISHED'}`, report success (e.g. “Migration complete”) and return `{'FINISHED'}`.\n\nSo the full migration is exactly “invoke the six step operators in sequence.”\n\n## 3. UI in [ui/panels.py](T:\\Dynamic-Link-Manager\\ui\\panels.py)\n\nIn the Character Migrator box, after the pair fields and pickers:\n\n- Keep one **“Run migration”** button that calls `dlm.run_character_migration` (runs all steps).\n- Add **six buttons**, one per step, e.g.:\n - “Step 1: Copy attributes” → `dlm.migrator_step_1`\n - “Step 2: Migrate NLA” → `dlm.migrator_step_2`\n - “Step 3: Custom properties” → `dlm.migrator_step_3`\n - “Step 4: Bone constraints” → `dlm.migrator_step_4`\n - “Step 5: Retarget relations” → `dlm.migrator_step_5`\n - “Step 6: Base body override” → `dlm.migrator_step_6`\n\nLayout can be one row for “Run migration” and then a row or column of the six step buttons (or two rows of three), depending on space.\n\n## 4. No changes to [ops/migrator.py](T:\\Dynamic-Link-Manager\\ops\\migrator.py)\n\nKeep `get_pair_manual`, `get_pair_automatic`, `run_step_1` … `run_step_6`, and `run_full_migration`. The add-on will use the step functions from the new operators and will use “run all” by invoking the six operators; `run_full_migration` can remain for any future or script use but is not required for the UI.\n\n## Summary\n\n- **New:** 6 operators in `ui/operators.py`, each resolving the pair and calling one `run_step_N`.\n- **Changed:** `DLM_OT_run_character_migration` only invokes those 6 operators in order.\n- **New UI:** 6 step buttons in the CharMig panel plus the existing “Run migration” button.\n- **Unchanged:** `ops/migrator.py`, pair resolution and step logic.\n", + "name": "Per-step migrator operators", + "overview": "Add one Blender operator per migrator step (steps 1–6), each with its own button in the CharMig UI, and change run_character_migration to invoke these six operators in sequence so the full migration is reproducible and each step can be run individually." +} +``` +Additional data: + +```json +{ + "reviewData": { + "status": "Requested", + "selectedOption": "none", + "isShowingInput": false + }, + "planUri": "file:///c%3A/Users/Nathan/.cursor/plans/per-step_migrator_operators_4986daf6.plan.md", + "hasOpenedEditor": true +} +``` +Result: + +```json +{ + "rejected": {} +} +``` +Status: **completed** + + +
+
+ +--- + +_**User**_ + +each step should be named after the step from the original specstory. + +--- + +_**Agent (model default, mode Agent)**_ + +
Thought Process + +The user wants the step operators (and presumably their UI labels) to be named after the steps from the original spec story. Let me check the plan and the spec story for the exact step names used there. + +From the plan file I created and the earlier context, the spec story steps were: +1. Copy attributes (location, rotation, scale) +2. Migrate NLA +3. Custom properties +4. Bone constraints (with cross-character retarget, trim duplicates) +5. Retarget relations +6. Replacement base body (library override + shape-key action) +7. (inside 4) Remove duplicate constraints + +The spec story transcript might use slightly different wording. Let me search for how the steps were referred to in the spec. +
+ +--- + + + + + +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "Step [0-9]|step [0-9]", + "path": "a:\\1 Amazon_Active_Projects\\260206_PAE_2026\\.specstory\\history\\2026-02-17_17-20Z-linked-library-override-resync-issue.md", + "contextBefore": 0, + "contextAfter": 0, + "caseInsensitive": false +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "Step [0-9]|step [0-9]", + "path": "a:\\1 Amazon_Active_Projects\\260206_PAE_2026\\.specstory\\history\\2026-02-17_17-20Z-linked-library-override-resync-issue.md", + "outputMode": "content", + "totalFiles": 1, + "totalMatches": 102, + "topFiles": [ + { + "uri": "a:\\1 Amazon_Active_Projects\\260206_PAE_2026\\.specstory\\history\\2026-02-17_17-20Z-linked-library-override-resync-issue.md", + "matchCount": 40 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + +
Thought Process + +From the spec story the steps are referred to as: +- Step 1: Copy attributes +- Step 2: NLA (migrate NLA / mirror tracks) +- Step 3: Custom properties (pose-bone custom properties) +- Step 4: Bone constraints (copy and retarget) +- Step 5: Retarget relations (parent, constraint target, Armature modifier) +- Step 6: Replacement base body (override + shape-key action) +- Step 7: (inside 4) Remove duplicate constraints + +The plan document (character_migrator_integration) had a table: +| **1** | Copy armature object attributes: location, rotation (quaternion/euler), scale from original to replacement. | +| **2** | Migrate NLA: copy tracks and strips... | +| **3** | Custom properties: copy pose-bone custom props... | +| **4** | Bone constraints: (a) On replacement, remove... (b) Copy constraints... (c) Trim... | +| **5** | Retarget relations: ... | +| **6** | Replacement base body: ... | +| **7** | Implemented inside step 4: remove duplicate constraints | + +So the canonical names from the spec/plan are: +1. Copy attributes (or "Copy armature object attributes") +2. Migrate NLA +3. Custom properties +4. Bone constraints +5. Retarget relations +6. Replacement base body + +For operator bl_idname we need valid identifiers (lowercase, underscores). So: +- dlm.migrator_copy_attributes +- dlm.migrator_migrate_nla +- dlm.migrator_custom_properties +- dlm.migrator_bone_constraints +- dlm.migrator_retarget_relations +- dlm.migrator_replacement_base_body + +And bl_label can be the human-readable spec name: +- "Copy attributes" +- "Migrate NLA" +- "Custom properties" +- "Bone constraints" +- "Retarget relations" +- "Replacement base body" + +I'll update the plan file to use these names. +
+ +--- + + + + + +--- + + +
Tool use: **read_file_v2** • Read file: c:\Users\Nathan\.cursor\plans\per-step_migrator_operators_4986daf6.plan.md + +
+
+ +--- + +
Thought Process + +I need to update the plan so that each step is named after the spec story. From the spec story the steps are: +1. Copy attributes +2. Migrate NLA (or "NLA") +3. Custom properties +4. Bone constraints +5. Retarget relations +6. Replacement base body + +So the operator bl_idnames should be based on these names (snake_case for id): +- dlm.migrator_copy_attributes +- dlm.migrator_migrate_nla +- dlm.migrator_custom_properties +- dlm.migrator_bone_constraints +- dlm.migrator_retarget_relations +- dlm.migrator_replacement_base_body + +And bl_label / button text: +- Copy attributes +- Migrate NLA +- Custom properties +- Bone constraints +- Retarget relations +- Replacement base body + +I'll update the plan file to use these names throughout (operator ids, class names, and UI labels). +
+ +--- + + +Updating the plan to name each step after the spec story (Copy attributes, Migrate NLA, Custom properties, Bone constraints, Retarget relations, Replacement base body). + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: c:\Users\Nathan\.cursor\plans\per-step_migrator_operators_4986daf6.plan.md + +
+ +
+ +--- + + + +
Tool use: **edit_file_v2** • Edit file: c:\Users\Nathan\.cursor\plans\per-step_migrator_operators_4986daf6.plan.md + +
+ +
+ +--- + + + +
Tool use: **edit_file_v2** • Edit file: c:\Users\Nathan\.cursor\plans\per-step_migrator_operators_4986daf6.plan.md + +
+ +
+ +--- + + + +
Tool use: **edit_file_v2** • Edit file: c:\Users\Nathan\.cursor\plans\per-step_migrator_operators_4986daf6.plan.md + +
+ +
+ +--- + +
Thought Process + +Fixed the typo in the plan - I had "utils__init__.py" in one place, should be "utils/__init__.py". Let me check if that's in the plan. +
+ +--- + + + + + +--- + + +
+Tool use: **ripgrep_raw_search** + + +Parameters: + +```json +{ + "pattern": "utils", + "path": "c:\\Users\\Nathan\\.cursor\\plans\\per-step_migrator_operators_4986daf6.plan.md", + "caseInsensitive": false +} +``` +Additional data: + +```json +{ + "isPruned": true, + "pattern": "utils", + "path": "c:\\Users\\Nathan\\.cursor\\plans\\per-step_migrator_operators_4986daf6.plan.md", + "outputMode": "content", + "totalFiles": 1, + "totalMatches": 1, + "topFiles": [ + { + "uri": "c:\\Users\\Nathan\\.cursor\\plans\\per-step_migrator_operators_4986daf6.plan.md", + "matchCount": 1 + } + ] +} +``` +Status: **completed** + + +
+
+ +--- + +
Thought Process + +There's a typo: utils__init__.py should be utils/__init__.py in the link. Let me fix that. +
+ +--- + + + + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: c:\Users\Nathan\.cursor\plans\per-step_migrator_operators_4986daf6.plan.md + +
+ +
+ +--- + +
Thought Process + +Plan is updated. Summary for the user. +
+ +--- + + +Summary of updates to the plan: + +**Step names (from spec story)** +Each step is now identified by the spec story name, and the plan uses these consistently: + +| Step | bl_idname | bl_label / button | +|------|-----------|--------------------| +| 1 | `dlm.migrator_copy_attributes` | Copy attributes | +| 2 | `dlm.migrator_migrate_nla` | Migrate NLA | +| 3 | `dlm.migrator_custom_properties` | Custom properties | +| 4 | `dlm.migrator_bone_constraints` | Bone constraints | +| 5 | `dlm.migrator_retarget_relations` | Retarget relations | +| 6 | `dlm.migrator_replacement_base_body` | Replacement base body | + +**Plan changes** +- Added a “Step names (from spec story)” section with the table above. +- Replaced generic `migrator_step_1` … `migrator_step_6` with these ids and labels everywhere (operator classes, `bl_idname`, `bl_label`, UI buttons). +- `run_character_migration` is specified to call `bpy.ops.dlm.migrator_copy_attributes()`, `bpy.ops.dlm.migrator_migrate_nla()`, etc., in that order. +- Fixed the utils link typo (`utils__init__.py` → `utils/__init__.py`). + +So each step is named after the original spec story (Copy attributes, Migrate NLA, Custom properties, Bone constraints, Retarget relations, Replacement base body) in both code and UI. + +--- + +_**User**_ + +Implement the plan as specified, it is attached for your reference. Do NOT edit the plan file itself. + +To-do's from the plan have already been created. Do not create them again. Mark them as in_progress as you work, starting with the first one. Don't stop until you have completed all the to-dos. + +--- + +_**Agent (model default, mode Agent)**_ + +
Thought Process + +The user wants me to implement the per-step migrator operators plan. Let me check the todos and then implement: + +1. Add six operators in ui/operators.py with the spec story names (copy_attributes, migrate_nla, custom_properties, bone_constraints, retarget_relations, replacement_base_body) +2. Change run_character_migration to invoke those six operators in order +3. Add six buttons in ui/panels.py + +I'll start by reading the current operators and panels to implement the changes. +
+ +--- + + + + + +--- + + +
Tool use: **todo_write** • Todo List + +- [-] Add six migrator step operators in ui/operators.py +- [ ] Change run_character_migration to invoke six operators +- [ ] Add six step buttons in CharMig panel +- [x] Migrator props and operators (run migration, pickers) +- [x] UI: CharMig section + Linked Libraries dropdown +- [x] Update blender_manifest.toml and __init__ registration + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py + +
+
+ +--- + + +
Tool use: **read_file_v2** • Read file: T:\Dynamic-Link-Manager\ui\operators.py + +
+
+ +--- + +
Thought Process + +I need to: +1. Add a helper that gets (orig, rep) from context (used by all step operators) +2. Add six operator classes: DLM_OT_migrator_copy_attributes, DLM_OT_migrator_migrate_nla, DLM_OT_migrator_custom_properties, DLM_OT_migrator_bone_constraints, DLM_OT_migrator_retarget_relations, DLM_OT_migrator_replacement_base_body +3. Change DLM_OT_run_character_migration to call those six operators in order +4. Add the six to OPERATOR_CLASSES +5. Update panels to add the six buttons + +Let me implement the six operators. Each one: +- Gets props, use_auto = props.migrator_mode, orig, rep = get_pair_automatic if use_auto else get_pair_manual +- If not orig or not rep or orig == rep: report error, return CANCELLED +- Try: call run_step_N (with orig_to_rep and rep_descendants for steps 4, 5, 6), report success, return FINISHED +- Except: report exception, return CANCELLED +
+ +--- + + +Adding the six step operators and a shared pair-resolve helper: + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py + +
+ +
+ +--- + +
Thought Process + +I made a mistake - I left duplicate/broken code in run_character_migration. There was a loop that called migrator_copy_attributes 6 times, then I added the op_id_to_call dict and another loop. Let me fix run_character_migration to only have the single loop that calls each operator by id. +
+ +--- + + +Fixing run_character_migration: removing the duplicate loop and incorrect call. + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py + +
+ +
+ +--- + +
Thought Process + +Now add the six operator classes to OPERATOR_CLASSES. +
+ +--- + + + + + +--- + + + +
Tool use: **edit_file_v2** • Edit file: T:\Dynamic-Link-Manager\ui\operators.py
diff --git a/ui/operators.py b/ui/operators.py index 3167329..42523d3 100644 --- a/ui/operators.py +++ b/ui/operators.py @@ -242,20 +242,175 @@ class DLM_OT_relocate_single_library(Operator): return {"RUNNING_MODAL"} +def _get_migrator_pair(context): + """Return (orig, rep) from scene props (manual or automatic). (None, None) if invalid.""" + from ..ops.migrator import get_pair_manual, get_pair_automatic + + props = getattr(context.scene, "dynamic_link_manager", None) + if not props: + return None, None + use_auto = getattr(props, "migrator_mode", False) + orig, rep = get_pair_automatic(context) if use_auto else get_pair_manual(context) + return orig, rep + + +class DLM_OT_migrator_copy_attributes(Operator): + bl_idname = "dlm.migrator_copy_attributes" + bl_label = "Copy attributes" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + orig, rep = _get_migrator_pair(context) + if not orig or not rep or orig == rep: + self.report({"ERROR"}, "No valid character pair (set Original/Replacement or enable Automatic).") + return {"CANCELLED"} + try: + from ..ops.migrator import run_step_1 + run_step_1(orig, rep) + self.report({"INFO"}, "Copy attributes done.") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + +class DLM_OT_migrator_migrate_nla(Operator): + bl_idname = "dlm.migrator_migrate_nla" + bl_label = "Migrate NLA" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + orig, rep = _get_migrator_pair(context) + if not orig or not rep or orig == rep: + self.report({"ERROR"}, "No valid character pair.") + return {"CANCELLED"} + try: + from ..ops.migrator import run_step_2 + run_step_2(orig, rep) + self.report({"INFO"}, "Migrate NLA done.") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + +class DLM_OT_migrator_custom_properties(Operator): + bl_idname = "dlm.migrator_custom_properties" + bl_label = "Custom properties" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + orig, rep = _get_migrator_pair(context) + if not orig or not rep or orig == rep: + self.report({"ERROR"}, "No valid character pair.") + return {"CANCELLED"} + try: + from ..ops.migrator import run_step_3 + run_step_3(orig, rep) + self.report({"INFO"}, "Custom properties done.") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + +class DLM_OT_migrator_bone_constraints(Operator): + bl_idname = "dlm.migrator_bone_constraints" + bl_label = "Bone constraints" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + orig, rep = _get_migrator_pair(context) + if not orig or not rep or orig == rep: + self.report({"ERROR"}, "No valid character pair.") + return {"CANCELLED"} + try: + from ..ops.migrator import run_step_4 + orig_to_rep = {orig: rep} + run_step_4(orig, rep, orig_to_rep) + self.report({"INFO"}, "Bone constraints done.") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + +class DLM_OT_migrator_retarget_relations(Operator): + bl_idname = "dlm.migrator_retarget_relations" + bl_label = "Retarget relations" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + orig, rep = _get_migrator_pair(context) + if not orig or not rep or orig == rep: + self.report({"ERROR"}, "No valid character pair.") + return {"CANCELLED"} + try: + from ..ops.migrator import run_step_5 + from ..utils import descendants + rep_descendants = descendants(rep) + orig_to_rep = {orig: rep} + run_step_5(orig, rep, rep_descendants, orig_to_rep) + self.report({"INFO"}, "Retarget relations done.") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + +class DLM_OT_migrator_replacement_base_body(Operator): + bl_idname = "dlm.migrator_replacement_base_body" + bl_label = "Replacement base body" + bl_options = {"REGISTER", "UNDO"} + + def execute(self, context): + orig, rep = _get_migrator_pair(context) + if not orig or not rep or orig == rep: + self.report({"ERROR"}, "No valid character pair.") + return {"CANCELLED"} + try: + from ..ops.migrator import run_step_6 + from ..utils import descendants + rep_descendants = descendants(rep) + run_step_6(orig, rep, rep_descendants) + self.report({"INFO"}, "Replacement base body done.") + return {"FINISHED"} + except Exception as e: + self.report({"ERROR"}, str(e)) + return {"CANCELLED"} + + +MIGRATOR_STEP_OPS = ( + "dlm.migrator_copy_attributes", + "dlm.migrator_migrate_nla", + "dlm.migrator_custom_properties", + "dlm.migrator_bone_constraints", + "dlm.migrator_retarget_relations", + "dlm.migrator_replacement_base_body", +) + + class DLM_OT_run_character_migration(Operator): bl_idname = "dlm.run_character_migration" bl_label = "Run Character Migration" bl_options = {"REGISTER", "UNDO"} def execute(self, context): - from ..ops.migrator import run_full_migration - - ok, msg = run_full_migration(context) - if ok: - self.report({"INFO"}, msg) - return {"FINISHED"} - self.report({"ERROR"}, msg) - return {"CANCELLED"} + steps = [ + bpy.ops.dlm.migrator_copy_attributes, + bpy.ops.dlm.migrator_migrate_nla, + bpy.ops.dlm.migrator_custom_properties, + bpy.ops.dlm.migrator_bone_constraints, + bpy.ops.dlm.migrator_retarget_relations, + bpy.ops.dlm.migrator_replacement_base_body, + ] + for i, op in enumerate(steps): + result = op() + if result != {"FINISHED"}: + self.report({"ERROR"}, f"Migration failed at step {i + 1}: {MIGRATOR_STEP_OPS[i]}") + return {"CANCELLED"} + self.report({"INFO"}, "Migration complete.") + return {"FINISHED"} class DLM_OT_picker_original_character(Operator): @@ -304,4 +459,10 @@ OPERATOR_CLASSES = [ DLM_OT_run_character_migration, DLM_OT_picker_original_character, DLM_OT_picker_replacement_character, + DLM_OT_migrator_copy_attributes, + DLM_OT_migrator_migrate_nla, + DLM_OT_migrator_custom_properties, + DLM_OT_migrator_bone_constraints, + DLM_OT_migrator_retarget_relations, + DLM_OT_migrator_replacement_base_body, ] diff --git a/ui/panels.py b/ui/panels.py index 2c84a99..da1f0b5 100644 --- a/ui/panels.py +++ b/ui/panels.py @@ -85,6 +85,14 @@ class DLM_PT_main_panel(Panel): row.operator("dlm.picker_replacement_character", text="", icon="EYEDROPPER") row = box.row() row.operator("dlm.run_character_migration", text="Run migration", icon="ARMATURE_DATA") + row = box.row(align=True) + row.operator("dlm.migrator_copy_attributes", text="Copy attributes") + row.operator("dlm.migrator_migrate_nla", text="Migrate NLA") + row.operator("dlm.migrator_custom_properties", text="Custom properties") + row = box.row(align=True) + row.operator("dlm.migrator_bone_constraints", text="Bone constraints") + row.operator("dlm.migrator_retarget_relations", text="Retarget relations") + row.operator("dlm.migrator_replacement_base_body", text="Replacement base body") # Linked Libraries: header row (always), main box only when expanded missing_count = sum(1 for lib in props.linked_libraries if lib.is_missing)