1020 lines
37 KiB
Markdown
1020 lines
37 KiB
Markdown
<!-- Generated by SpecStory, Markdown v2.1.0 -->
|
|
|
|
<!-- cursor Session 577872ce-9067-4be5-bdb0-89d48d9424ca (2026-03-10 16:25Z) -->
|
|
|
|
# Collection removal error handling (2026-03-10 16:25Z)
|
|
|
|
_**User**_
|
|
|
|
Fix the error in DLM_OT_migrator_remove_original in T:\Dynamic-Link-Manager\ui\operators.py.
|
|
|
|
The error is: `Failed to remove original: StructRNA of type Collection has been removed`
|
|
|
|
This happens because we're holding a reference to a Collection RNA struct that gets invalidated. The fix:
|
|
|
|
1. Store the collection name before removal
|
|
2. Use bpy.data.collections.remove() with the stored reference
|
|
3. But be careful - if the collection is already being removed by some other process, handle that gracefully
|
|
|
|
Current code:
|
|
```python
|
|
coll = collection_containing_armature(orig)
|
|
if coll:
|
|
bpy.data.collections.remove(coll)
|
|
```
|
|
|
|
Change it to store the name first, then remove by the actual reference but wrap in try-except. Also, the issue might be that we're trying to remove a collection that's already linked/being removed. Let me check what the proper safe removal pattern is.
|
|
|
|
Actually, looking at the error - the issue is likely that `collection_containing_armature(orig)` returns a reference, but by the time we try to remove it, something has invalidated it. The fix should be:
|
|
|
|
```python
|
|
coll = collection_containing_armature(orig)
|
|
if coll:
|
|
coll_name = coll.name # Store name before removal
|
|
try:
|
|
bpy.data.collections.remove(coll)
|
|
self.report({"INFO"}, f"Removed collection: {coll_name}")
|
|
except ReferenceError:
|
|
# Collection was already removed
|
|
self.report({"WARNING"}, f"Collection {coll_name} was already removed")
|
|
```
|
|
|
|
But actually ReferenceError is for Python weakrefs, not Blender RNA. For Blender RNA we need RuntimeError or Exception.
|
|
|
|
Make the fix that:
|
|
1. Stores the collection name
|
|
2. Tries to remove the collection
|
|
3. Catches any exception and falls back to just removing the object
|
|
4. Always clears the property and reports what happened
|
|
|
|
---
|
|
|
|
_**Agent (model accounts/fireworks/models/kimi-k2p5, mode Agent)**_
|
|
|
|
|
|
|
|
---
|
|
|
|
<think><details><summary>Thought Process</summary>
|
|
The user wants me to fix an error in the `DLM_OT_migrator_remove_original` operator in `T:\Dynamic-Link-Manager\ui\operators.py`. The error is about trying to remove a collection that has already been invalidated.
|
|
|
|
Let me first read the current code to see the full context, then apply the fix.</details></think>
|
|
|
|
---
|
|
|
|
<tool-use data-tool-type="shell" data-tool-name="run_terminal_command_v2">
|
|
<details><summary>Tool use: **run_terminal_command_v2** • Run command: cat "T:\Dynamic-Link-Manager\ui\operators.py"</summary>
|
|
|
|
```bash
|
|
cat "T:\Dynamic-Link-Manager\ui\operators.py"
|
|
```
|
|
|
|
```
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
import bpy
|
|
import os
|
|
from bpy.types import Operator
|
|
from bpy.props import StringProperty, BoolProperty
|
|
from bpy.props import StringProperty, IntProperty
|
|
|
|
from ..utils import collection_containing_armature
|
|
|
|
ADDON_NAME = __package__.rsplit(".", 1)[0] if "." in __package__ else __package__
|
|
|
|
|
|
def _prefs(context):
|
|
return context.preferences.addons.get(ADDON_NAME)
|
|
|
|
|
|
class DLM_OT_replace_linked_asset(Operator):
|
|
bl_idname = "dlm.replace_linked_asset"
|
|
bl_label = "Replace Linked Asset"
|
|
bl_description = "Open file browser to replace the linked asset with another file"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
filepath: StringProperty(name="File Path", description="Path to the new asset file", subtype="FILE_PATH")
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
if not obj:
|
|
self.report({"ERROR"}, "No object selected")
|
|
return {"CANCELLED"}
|
|
if getattr(obj, "library", None):
|
|
self.report({"INFO"}, f"Object '{obj.name}' is linked from: {obj.library.filepath}")
|
|
return {"FINISHED"}
|
|
if obj.data and getattr(obj.data, "library", None) and obj.data.library:
|
|
self.report({"INFO"}, f"Object '{obj.name}' has linked data from: {obj.data.library.filepath}")
|
|
return {"FINISHED"}
|
|
if obj.type == "ARMATURE" and obj.data and obj.data.name in bpy.data.armatures:
|
|
ad = bpy.data.armatures[obj.data.name]
|
|
if getattr(ad, "library", None) and ad.library:
|
|
self.report({"INFO"}, f"Armature '{obj.name}' data is linked from: {ad.library.filepath}")
|
|
return {"FINISHED"}
|
|
self.report({"ERROR"}, "Selected object is not a linked asset")
|
|
return {"CANCELLED"}
|
|
|
|
def invoke(self, context, event):
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class DLM_OT_scan_linked_assets(Operator):
|
|
bl_idname = "dlm.scan_linked_assets"
|
|
bl_label = "Scan Linked Libraries"
|
|
bl_description = "Scan the current file for linked libraries and list their status"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
from ..ops import library
|
|
|
|
return library.scan_linked_assets(context, self.report)
|
|
|
|
|
|
class DLM_OT_find_libraries_in_folders(Operator):
|
|
bl_idname = "dlm.find_libraries_in_folders"
|
|
bl_label = "Find Libraries in Folders"
|
|
bl_description = "Search addon search paths for missing library blend files"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
from ..ops import library
|
|
|
|
return library.find_libraries_in_folders(context, self.report, ADDON_NAME)
|
|
|
|
|
|
class DLM_OT_open_linked_file(Operator):
|
|
bl_idname = "dlm.open_linked_file"
|
|
bl_label = "Open Linked File"
|
|
bl_description = "Open the selected linked blend file in a new Blender instance"
|
|
bl_options = {"REGISTER"}
|
|
filepath: StringProperty(name="File Path", default="")
|
|
|
|
def execute(self, context):
|
|
if not self.filepath:
|
|
self.report({"ERROR"}, "No file path specified")
|
|
return {"CANCELLED"}
|
|
try:
|
|
bpy.ops.wm.path_open(filepath=self.filepath)
|
|
self.report({"INFO"}, f"Opening linked file: {self.filepath}")
|
|
except Exception as e:
|
|
self.report({"ERROR"}, f"Failed to open linked file: {e}")
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_add_search_path(Operator):
|
|
bl_idname = "dlm.add_search_path"
|
|
bl_label = "Add Search Path"
|
|
bl_description = "Add a new folder to the list of search paths for finding libraries"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
prefs = _prefs(context)
|
|
if prefs:
|
|
p = prefs.preferences.search_paths.add()
|
|
p.path = "//"
|
|
self.report({"INFO"}, f"Added search path: {p.path}")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_remove_search_path(Operator):
|
|
bl_idname = "dlm.remove_search_path"
|
|
bl_label = "Remove Search Path"
|
|
bl_description = "Remove the selected search path from the list"
|
|
bl_options = {"REGISTER"}
|
|
index: IntProperty(name="Index", default=0)
|
|
|
|
def execute(self, context):
|
|
prefs = _prefs(context)
|
|
if prefs and prefs.preferences.search_paths and 0 <= self.index < len(prefs.preferences.search_paths):
|
|
prefs.preferences.search_paths.remove(self.index)
|
|
self.report({"INFO"}, f"Removed search path at index {self.index}")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_attempt_relink(Operator):
|
|
bl_idname = "dlm.attempt_relink"
|
|
bl_label = "Attempt Relink"
|
|
bl_description = "Try to relink missing libraries using the configured search paths"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
from ..ops import library
|
|
|
|
return library.attempt_relink(context, self.report, ADDON_NAME)
|
|
|
|
|
|
class DLM_OT_browse_search_path(Operator):
|
|
bl_idname = "dlm.browse_search_path"
|
|
bl_label = "Browse Search Path"
|
|
bl_description = "Browse to set the folder for the selected search path"
|
|
bl_options = {"REGISTER"}
|
|
index: IntProperty(name="Index", default=0)
|
|
filepath: StringProperty(name="Search Path", subtype="DIR_PATH")
|
|
|
|
def execute(self, context):
|
|
prefs = _prefs(context)
|
|
if prefs and prefs.preferences.search_paths and 0 <= self.index < len(prefs.preferences.search_paths):
|
|
prefs.preferences.search_paths[self.index].path = self.filepath
|
|
self.report({"INFO"}, f"Updated search path {self.index + 1}: {self.filepath}")
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
prefs = _prefs(context)
|
|
if prefs and prefs.preferences.search_paths and 0 <= self.index < len(prefs.preferences.search_paths):
|
|
self.filepath = prefs.preferences.search_paths[self.index].path
|
|
context.window_manager.fileselect_add(self)
|
|
return {"RUNNING_MODAL"}
|
|
|
|
|
|
class DLM_OT_reload_libraries(Operator):
|
|
bl_idname = "dlm.reload_libraries"
|
|
bl_label = "Reload Libraries"
|
|
bl_description = "Reload all linked libraries (or fallback manual reload)"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
bpy.ops.outliner.lib_operation(type="RELOAD")
|
|
self.report({"INFO"}, "Library reload operation completed")
|
|
except Exception:
|
|
try:
|
|
for lib in bpy.data.libraries:
|
|
if lib.filepath and os.path.exists(bpy.path.abspath(lib.filepath)):
|
|
lib.reload()
|
|
self.report({"INFO"}, "Libraries reloaded manually")
|
|
except Exception as e:
|
|
self.report({"ERROR"}, f"Failed to reload libraries: {e}")
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_make_paths_relative(Operator):
|
|
bl_idname = "dlm.make_paths_relative"
|
|
bl_label = "Make Paths Relative"
|
|
bl_description = "Convert all internal file paths to relative"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
bpy.ops.file.make_paths_relative()
|
|
self.report({"INFO"}, "All file paths made relative")
|
|
except Exception as e:
|
|
self.report({"ERROR"}, f"Failed to make paths relative: {e}")
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_make_paths_absolute(Operator):
|
|
bl_idname = "dlm.make_paths_absolute"
|
|
bl_label = "Make Paths Absolute"
|
|
bl_description = "Convert all internal file paths to absolute"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
try:
|
|
bpy.ops.file.make_paths_absolute()
|
|
self.report({"INFO"}, "All file paths made absolute")
|
|
except Exception as e:
|
|
self.report({"ERROR"}, f"Failed to make paths absolute: {e}")
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_relocate_single_library(Operator):
|
|
bl_idname = "dlm.relocate_single_library"
|
|
bl_label = "Relocate Library"
|
|
bl_description = "Point the selected library to a new blend file and reload"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
target_filepath: StringProperty(name="Current Library Path", default="")
|
|
filepath: StringProperty(name="New Library File", subtype="FILE_PATH", default="")
|
|
|
|
def execute(self, context):
|
|
if not self.target_filepath or not self.filepath:
|
|
self.report({"ERROR"}, "No target or new file specified")
|
|
return {"CANCELLED"}
|
|
abs_match = bpy.path.abspath(self.target_filepath) if self.target_filepath else ""
|
|
library = None
|
|
for lib in bpy.data.libraries:
|
|
try:
|
|
if lib.filepath and bpy.path.abspath(lib.filepath) == abs_match:
|
|
library = lib
|
|
break
|
|
except Exception:
|
|
if lib.filepath == self.target_filepath:
|
|
library = lib
|
|
break
|
|
if not library:
|
|
self.report({"ERROR"}, "Could not resolve the selected library")
|
|
return {"CANCELLED"}
|
|
try:
|
|
library.filepath = self.filepath
|
|
library.reload()
|
|
self.report({"INFO"}, f"Relocated to: {self.filepath}")
|
|
except Exception as e:
|
|
self.report({"ERROR"}, f"Failed to relocate: {e}")
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
def invoke(self, context, event):
|
|
if self.target_filepath:
|
|
try:
|
|
self.filepath = bpy.path.abspath(self.target_filepath)
|
|
except Exception:
|
|
self.filepath = self.target_filepath
|
|
context.window_manager.fileselect_add(self)
|
|
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 = "CopyAttr"
|
|
bl_description = "Copy object and armature attributes from original to replacement character"
|
|
bl_icon = "COPY_ID"
|
|
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_copy_attr
|
|
run_copy_attr(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 = "MigNLA"
|
|
bl_description = "Migrate NLA tracks and strips from original to replacement character"
|
|
bl_icon = "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_mig_nla
|
|
run_mig_nla(orig, rep, report=self.report)
|
|
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 = "MigCustProps"
|
|
bl_description = "Copy custom properties from original to replacement character"
|
|
bl_icon = "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_mig_cust_props
|
|
run_mig_cust_props(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 = "MigBoneConst"
|
|
bl_description = "Migrate bone constraints from original to replacement armature"
|
|
bl_icon = "CONSTRAINT_BONE"
|
|
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_mig_bone_const
|
|
orig_to_rep = {orig: rep}
|
|
run_mig_bone_const(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 = "RetargRelatives"
|
|
bl_description = "Retarget parent/child and other relations to the replacement character"
|
|
bl_icon = "ORIENTATION_PARENT"
|
|
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_retarg_relatives
|
|
from ..utils import descendants
|
|
rep_descendants = descendants(rep)
|
|
orig_to_rep = {orig: rep}
|
|
run_retarg_relatives(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_basebody_shapekeys(Operator):
|
|
bl_idname = "dlm.migrator_basebody_shapekeys"
|
|
bl_label = "MigBBodyShapeKeys"
|
|
bl_description = "Migrate base body mesh shape key values from original to replacement"
|
|
bl_icon = "SHAPEKEY_DATA"
|
|
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_mig_bbody_shapekeys
|
|
from ..utils import descendants
|
|
rep_descendants = descendants(rep)
|
|
run_mig_bbody_shapekeys(orig, rep, rep_descendants, context)
|
|
self.report({"INFO"}, "Migrate BaseBody shapekeys done.")
|
|
return {"FINISHED"}
|
|
except Exception as e:
|
|
self.report({"ERROR"}, str(e))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class DLM_OT_migrator_fk_rotations(Operator):
|
|
bl_idname = "dlm.migrator_fk_rotations"
|
|
bl_label = "MigFKRot"
|
|
bl_description = "Copy FK arm and finger rotations from original to replacement (uses constraints)"
|
|
bl_icon = "BONE_DATA"
|
|
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.fk_rotations import copy_fk_rotations
|
|
ok, msg = copy_fk_rotations(context, orig, rep)
|
|
if ok:
|
|
self.report({"INFO"}, msg)
|
|
return {"FINISHED"}
|
|
else:
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
except Exception as e:
|
|
self.report({"ERROR"}, str(e))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class DLM_OT_migrator_fk_rotations_bake(Operator):
|
|
bl_idname = "dlm.migrator_fk_rotations_bake"
|
|
bl_label = "Bake MigFKRot"
|
|
bl_description = "Bake FK rotations to keyframes using nla.bake (similar to tweak tools)"
|
|
bl_icon = "KEYFRAME"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
track_name: StringProperty(name="NLA Track", description="Optional NLA track name for frame range", default="")
|
|
post_clean: BoolProperty(name="Post-clean", description="Clean curves after bake", default=False)
|
|
|
|
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.fk_rotations import bake_fk_rotations
|
|
ok, msg = bake_fk_rotations(context, orig, rep, track_name=self.track_name or None, post_clean=self.post_clean)
|
|
if ok:
|
|
self.report({"INFO"}, msg)
|
|
return {"FINISHED"}
|
|
else:
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
except Exception as e:
|
|
self.report({"ERROR"}, str(e))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class DLM_OT_migrator_fk_rotations_remove(Operator):
|
|
bl_idname = "dlm.migrator_fk_rotations_remove"
|
|
bl_label = "Remove MigFKRot"
|
|
bl_description = "Remove FK rotation COPY_TRANSFORMS constraints (similar to tweak_remove_arm)"
|
|
bl_icon = "X"
|
|
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.fk_rotations import remove_fk_rotations
|
|
ok, msg = remove_fk_rotations(context, rep)
|
|
if ok:
|
|
self.report({"INFO"}, msg)
|
|
return {"FINISHED"}
|
|
else:
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
except Exception as e:
|
|
self.report({"ERROR"}, str(e))
|
|
return {"CANCELLED"}
|
|
|
|
|
|
|
|
|
|
class DLM_OT_migrator_remove_original(Operator):
|
|
bl_idname = "dlm.migrator_remove_original"
|
|
bl_label = "Remove Original"
|
|
bl_description = "Delete the original character armature and its data from the scene"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
if not orig:
|
|
self.report({"WARNING"}, "No original character selected")
|
|
return {"CANCELLED"}
|
|
if orig == rep:
|
|
self.report({"ERROR"}, "Original and replacement cannot be the same object")
|
|
return {"CANCELLED"}
|
|
|
|
name = orig.name
|
|
try:
|
|
# Try to find and delete the collection containing the original character
|
|
coll = collection_containing_armature(orig)
|
|
if coll:
|
|
bpy.data.collections.remove(coll)
|
|
context.scene.dynamic_link_manager.original_character = None
|
|
self.report({"INFO"}, f"Removed collection: {coll.name}")
|
|
else:
|
|
# Fallback: just delete the armature object
|
|
bpy.data.objects.remove(orig, do_unlink=True)
|
|
context.scene.dynamic_link_manager.original_character = None
|
|
self.report({"INFO"}, f"Removed original character: {name}")
|
|
except Exception as e:
|
|
self.report({"ERROR"}, f"Failed to remove original: {e}")
|
|
return {"CANCELLED"}
|
|
return {"FINISHED"}
|
|
|
|
|
|
|
|
class DLM_OT_picker_original_character(Operator):
|
|
bl_idname = "dlm.picker_original_character"
|
|
bl_label = "Pick Original"
|
|
bl_description = "Set the original character armature from the active object"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
if not obj or obj.type != "ARMATURE":
|
|
self.report({"WARNING"}, "Select an armature")
|
|
return {"CANCELLED"}
|
|
context.scene.dynamic_link_manager.original_character = obj
|
|
self.report({"INFO"}, f"Original: {obj.name}")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_picker_replacement_character(Operator):
|
|
bl_idname = "dlm.picker_replacement_character"
|
|
bl_label = "Pick Replacement"
|
|
bl_description = "Set the replacement character armature from the active object"
|
|
bl_options = {"REGISTER"}
|
|
|
|
def execute(self, context):
|
|
obj = context.active_object
|
|
if not obj or obj.type != "ARMATURE":
|
|
self.report({"WARNING"}, "Select an armature")
|
|
return {"CANCELLED"}
|
|
context.scene.dynamic_link_manager.replacement_character = obj
|
|
self.report({"INFO"}, f"Replacement: {obj.name}")
|
|
return {"FINISHED"}
|
|
|
|
|
|
def _tweak_poll(context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
return orig is not None and rep is not None
|
|
|
|
|
|
class DLM_OT_tweak_add_arm(Operator):
|
|
bl_idname = "dlm.tweak_add_arm"
|
|
bl_label = "Add Arm Tweaks"
|
|
bl_description = "Add tweak bone constraints to arm bones on the replacement character"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
from ..ops import tweak_tools
|
|
tweak_tools.add_tweak_constraints(orig, rep, "arm")
|
|
self.report({"INFO"}, "Arm tweak constraints added.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_tweak_remove_arm(Operator):
|
|
bl_idname = "dlm.tweak_remove_arm"
|
|
bl_label = "Remove Arm Tweaks"
|
|
bl_description = "Remove arm tweak constraints from the replacement character"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
from ..ops import tweak_tools
|
|
n = tweak_tools.remove_tweak_constraints(orig, rep, "arm")
|
|
self.report({"INFO"}, f"Removed {n} arm tweak constraints.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_tweak_bake_arm(Operator):
|
|
bl_idname = "dlm.tweak_bake_arm"
|
|
bl_label = "Bake Arm Tweaks"
|
|
bl_description = "Bake arm tweak constraints to keyframes and optionally remove constraints"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
props = context.scene.dynamic_link_manager
|
|
from ..ops import tweak_tools
|
|
ok, msg = tweak_tools.bake_tweak_constraints(
|
|
context, orig, rep, "arm",
|
|
getattr(props, "tweak_nla_track_name", "") or "",
|
|
getattr(props, "tweak_bake_post_clean", False),
|
|
)
|
|
if ok:
|
|
self.report({"INFO"}, msg)
|
|
return {"FINISHED"}
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class DLM_OT_tweak_add_leg(Operator):
|
|
bl_idname = "dlm.tweak_add_leg"
|
|
bl_label = "Add Leg Tweaks"
|
|
bl_description = "Add tweak bone constraints to leg bones on the replacement character"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
from ..ops import tweak_tools
|
|
tweak_tools.add_tweak_constraints(orig, rep, "leg")
|
|
self.report({"INFO"}, "Leg tweak constraints added.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_tweak_remove_leg(Operator):
|
|
bl_idname = "dlm.tweak_remove_leg"
|
|
bl_label = "Remove Leg Tweaks"
|
|
bl_description = "Remove leg tweak constraints from the replacement character"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
from ..ops import tweak_tools
|
|
n = tweak_tools.remove_tweak_constraints(orig, rep, "leg")
|
|
self.report({"INFO"}, f"Removed {n} leg tweak constraints.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_tweak_bake_leg(Operator):
|
|
bl_idname = "dlm.tweak_bake_leg"
|
|
bl_label = "Bake Leg Tweaks"
|
|
bl_description = "Bake leg tweak constraints to keyframes and optionally remove constraints"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
props = context.scene.dynamic_link_manager
|
|
from ..ops import tweak_tools
|
|
ok, msg = tweak_tools.bake_tweak_constraints(
|
|
context, orig, rep, "leg",
|
|
getattr(props, "tweak_nla_track_name", "") or "",
|
|
getattr(props, "tweak_bake_post_clean", False),
|
|
)
|
|
if ok:
|
|
self.report({"INFO"}, msg)
|
|
return {"FINISHED"}
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
|
|
|
|
class DLM_OT_tweak_add_both(Operator):
|
|
bl_idname = "dlm.tweak_add_both"
|
|
bl_label = "Add Arm & Leg Tweaks"
|
|
bl_description = "Add tweak bone constraints to both arm and leg bones"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
from ..ops import tweak_tools
|
|
tweak_tools.add_tweak_constraints(orig, rep, "both")
|
|
self.report({"INFO"}, "Arm & leg tweak constraints added.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_tweak_remove_both(Operator):
|
|
bl_idname = "dlm.tweak_remove_both"
|
|
bl_label = "Remove Arm & Leg Tweaks"
|
|
bl_description = "Remove all arm and leg tweak constraints from the replacement character"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
from ..ops import tweak_tools
|
|
n = tweak_tools.remove_tweak_constraints(orig, rep, "both")
|
|
self.report({"INFO"}, f"Removed {n} tweak constraints.")
|
|
return {"FINISHED"}
|
|
|
|
|
|
class DLM_OT_tweak_bake_both(Operator):
|
|
bl_idname = "dlm.tweak_bake_both"
|
|
bl_label = "Bake Arm & Leg Tweaks"
|
|
bl_description = "Bake all arm and leg tweak constraints to keyframes and optionally remove constraints"
|
|
bl_options = {"REGISTER", "UNDO"}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return _tweak_poll(context)
|
|
|
|
def execute(self, context):
|
|
orig, rep = _get_migrator_pair(context)
|
|
props = context.scene.dynamic_link_manager
|
|
from ..ops import tweak_tools
|
|
ok, msg = tweak_tools.bake_tweak_constraints(
|
|
context, orig, rep, "both",
|
|
getattr(props, "tweak_nla_track_name", "") or "",
|
|
getattr(props, "tweak_bake_post_clean", False),
|
|
)
|
|
if ok:
|
|
self.report({"INFO"}, msg)
|
|
return {"FINISHED"}
|
|
self.report({"ERROR"}, msg)
|
|
return {"CANCELLED"}
|
|
|
|
|
|
OPERATOR_CLASSES = [
|
|
DLM_OT_replace_linked_asset,
|
|
DLM_OT_scan_linked_assets,
|
|
DLM_OT_find_libraries_in_folders,
|
|
DLM_OT_open_linked_file,
|
|
DLM_OT_add_search_path,
|
|
DLM_OT_remove_search_path,
|
|
DLM_OT_browse_search_path,
|
|
DLM_OT_attempt_relink,
|
|
DLM_OT_reload_libraries,
|
|
DLM_OT_make_paths_relative,
|
|
DLM_OT_make_paths_absolute,
|
|
DLM_OT_relocate_single_library,
|
|
DLM_OT_migrator_remove_original,
|
|
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_basebody_shapekeys,
|
|
DLM_OT_tweak_add_arm,
|
|
DLM_OT_tweak_remove_arm,
|
|
DLM_OT_tweak_bake_arm,
|
|
DLM_OT_tweak_add_leg,
|
|
DLM_OT_tweak_remove_leg,
|
|
DLM_OT_tweak_bake_leg,
|
|
DLM_OT_tweak_add_both,
|
|
DLM_OT_tweak_remove_both,
|
|
DLM_OT_tweak_bake_both,
|
|
DLM_OT_migrator_fk_rotations,
|
|
DLM_OT_migrator_fk_rotations_bake,
|
|
DLM_OT_migrator_fk_rotations_remove,
|
|
]
|
|
|
|
```
|
|
</details>
|
|
</tool-use>
|
|
|
|
---
|
|
|
|
<think><details><summary>Thought Process</summary>
|
|
I can see the `DLM_OT_migrator_remove_original` class in the file. Looking at the `execute` method, the issue is clear: after `bpy.data.collections.remove(coll)` is called, the code tries to access `coll.name` for the report, but `coll` is now an invalidated RNA reference. The error happens because:
|
|
|
|
1. `coll = collection_containing_armature(orig)` gets a reference to the collection
|
|
2. `bpy.data.collections.remove(coll)` removes the collection and invalidates the RNA struct
|
|
3. `self.report({"INFO"}, f"Removed collection: {coll.name}")` tries to access `coll.name` which now fails
|
|
|
|
The fix needs to:
|
|
1. Store the collection name before removal
|
|
2. Clear the property and report before attempting the remove (or catch exceptions properly)
|
|
|
|
Let me look at the exact lines to fix. The current code is around the middle of the file. Let me find the exact line numbers and then make the fix.</details></think>
|
|
|
|
---
|
|
|
|
I can see the issue in the `DLM_OT_migrator_remove_original.execute` method. The problem is that `coll.name` is accessed AFTER `bpy.data.collections.remove(coll)` has already invalidated the RNA reference.
|
|
|
|
Let me apply the fix by modifying the code to store the name first and handle exceptions gracefully.
|
|
|
|
---
|
|
|
|
<tool-use data-tool-type="shell" data-tool-name="run_terminal_command_v2">
|
|
<details><summary>Tool use: **run_terminal_command_v2** • Run command: sed -n '230,260p' "T:\Dynamic-Link-Manager\ui\operators.py"</summary>
|
|
|
|
```bash
|
|
sed -n '230,260p' "T:\Dynamic-Link-Manager\ui\operators.py"
|
|
```
|
|
|
|
```
|
|
sed: C:\Users\Nathan\AppData\Local\Temp\ps-script-3ae04e65-b0d2-472b-b4b5-8acedea11bd4.ps1:113
|
|
Line |
|
|
113 | sed -n '230,260p' "T:\Dynamic-Link-Manager\ui\operators.py"
|
|
| ~~~
|
|
| The term 'sed' is not recognized as a name of a cmdlet, function, script file, or executable program. Check the
|
|
| spelling of the name, or if a path was included, verify that the path is correct and try again.
|
|
|
|
```
|
|
</details>
|
|
</tool-use>
|
|
|
|
---
|
|
|
|
<think><details><summary>Thought Process</summary>
|
|
`sed` is not available on Windows. Let me use PowerShell or another approach to find the exact lines. Let me just use Python to read the file and find the lines.</details></think>
|
|
|
|
---
|
|
|
|
Let me use Python to find the exact line numbers and make the edit.
|
|
|
|
---
|
|
|
|
<tool-use data-tool-type="shell" data-tool-name="run_terminal_command_v2">
|
|
<details><summary>Tool use: **run_terminal_command_v2** • Run command: python -c "
|
|
with open(r'T:\Dynamic-Link-Manager\ui\operators.py', 'r') as f:
|
|
lines = f.readlines()
|
|
for i, line in enumerate(lines, 1):
|
|
if 'DLM_OT_migrator_remove_original' in line:
|
|
print(f'Class starts at line {i}: {line.strip()}')
|
|
if 'bpy.data.collections.remove' in line:
|
|
print(f'Remove call at line {i}: {line.strip()}')
|
|
"</summary>
|
|
|
|
```bash
|
|
python -c "
|
|
with open(r'T:\Dynamic-Link-Manager\ui\operators.py', 'r') as f:
|
|
lines = f.readlines()
|
|
for i, line in enumerate(lines, 1):
|
|
if 'DLM_OT_migrator_remove_original' in line:
|
|
print(f'Class starts at line {i}: {line.strip()}')
|
|
if 'bpy.data.collections.remove' in line:
|
|
print(f'Remove call at line {i}: {line.strip()}')
|
|
"
|
|
```
|
|
|
|
```
|
|
Class starts at line 492: class DLM_OT_migrator_remove_original(Operator):
|
|
Remove call at line 512: bpy.data.collections.remove(coll)
|
|
Class starts at line 763: DLM_OT_migrator_remove_original,
|
|
|
|
```
|
|
</details>
|
|
</tool-use>
|
|
|
|
---
|
|
|
|
<think><details><summary>Thought Process</summary>
|
|
Good, I found the line numbers. The class starts at line 492 and the `bpy.data.collections.remove(coll)` call is at line 512. Let me view the context around lines 492-525 to see the full execute method that needs to be fixed.</details></think>
|
|
|
|
---
|
|
|
|
<tool-use data-tool-type="shell" data-tool-name="run_terminal_command_v2">
|
|
<details><summary>Tool use: **run_terminal_command_v2** • Run command: python -c "
|
|
with open(r'T:\Dynamic-Link-Manager\ui\operators.py', 'r') as f:
|
|
lines = f.readlines()
|
|
for i in range(491, 525):
|
|
print(f'{i+1}: {lines[i]}', end='')
|
|
"</summary>
|
|
|
|
```bash
|
|
python -c "
|
|
with open(r'T:\Dynamic-Link-Manager\ui\operators.py', 'r') as f:
|
|
lines = f.readlines()
|
|
for i in range(491, 525):
|
|
print(f'{i+1}: {lines[i]}', end='')
|
|
"
|
|
```
|
|
|
|
```
|
|
492: class DLM_OT_migrator_remove_original(Operator):
|
|
493: bl_idname = "dlm.migrator_remove_original"
|
|
494: bl_label = "Remove Original"
|
|
495: bl_description = "Delete the original character armature and its data from the scene"
|
|
496: bl_options = {"REGISTER", "UNDO"}
|
|
497:
|
|
498: def execute(self, context):
|
|
499: orig, rep = _get_migrator_pair(context)
|
|
500: if not orig:
|
|
501: self.report({"WARNING"}, "No original character selected")
|
|
502: return {"CANCELLED"}
|
|
503: if orig == rep:
|
|
504: self.report({"ERROR"}, "Original and replacement cannot be the same object")
|
|
505: return {"CANCELLED"}
|
|
506:
|
|
507: name = orig.name
|
|
508: try:
|
|
509: # Try to find and delete the collection containing the original character
|
|
510: coll = collection_containing_armature(orig)
|
|
511: if coll:
|
|
512: bpy.data.collections.remove(coll)
|
|
513: context.scene.dynamic_link_manager.original_character = None
|
|
514: self.report({"INFO"}, f"Removed collection: {coll.name}")
|
|
515: else:
|
|
516: # Fallback: just delete the armature object
|
|
517: bpy.data.objects.remove(orig, do_unlink=True)
|
|
518: context.scene.dynamic_link_manager.original_character = None
|
|
519: self.report({"INFO"}, f"Removed original character: {name}")
|
|
520: except Exception as e:
|
|
521: self.report({"ERROR"}, f"Failed to remove original: {e}")
|
|
522: return {"CANCELLED"}
|
|
523: return {"FINISHED"}
|
|
524:
|
|
525:
|
|
|
|
```
|
|
</details>
|
|
</tool-use>
|
|
|
|
---
|
|
|
|
<think><details><summary>Thought Process</summary>
|
|
Now I can see the exact lines. The issue is:
|
|
- Line 512: `bpy.data.collections.remove(coll)` removes the collection and invalidates `coll`
|
|
- Line 514: `self.report({"INFO"}, f"Removed collection: {</details></think>
|
|
|
|
---
|
|
|