Utility_Apps/Blender/addons/DOESN'T WORK-Relink Broken Linked Object/DOESN'T WORK__init__.py
2025-09-10 09:31:12 -05:00

224 lines
7.5 KiB
Python

bl_info = {
"name": "Relink Broken Linked Object or Collection",
"author": "ChatGPT + Ryan",
"version": (1, 4),
"blender": (4, 0, 0),
"location": "3D View > Sidebar > Relink Tab",
"description": "Fix broken links by relinking objects or collections from a new .blend file",
"category": "Object",
}
import bpy
from bpy.types import Operator, Panel, PropertyGroup
from bpy.props import StringProperty, PointerProperty, BoolProperty
from bpy_extras.io_utils import ImportHelper
import difflib
def get_blend_objects_and_collections(filepath):
try:
with bpy.data.libraries.load(filepath, link=True) as (data_from, _):
objects = [(name, f"[OBJ] {name}", "") for name in data_from.objects]
collections = [(name, f"[COL] {name}", "") for name in data_from.collections]
return objects, collections
except Exception as e:
print(f"Error reading blend: {e}")
return [], []
def is_broken_link(obj):
# Typical check for broken linked empty object
return obj.library and obj.type == 'EMPTY' and obj.data is None
class SwapLinkProps(PropertyGroup):
old_object: StringProperty(name="Old Object")
blend_path: StringProperty(name="Blend File", subtype='FILE_PATH')
new_object: StringProperty(name="New Object")
new_collection: StringProperty(name="New Collection")
use_collection: BoolProperty(name="Use Collection", default=False)
class VIEW3D_OT_select_blend_file(Operator, ImportHelper):
bl_idname = "view3d.select_blend_file"
bl_label = "Select .blend File"
filename_ext = ".blend"
filter_glob: StringProperty(default="*.blend", options={'HIDDEN'})
def execute(self, context):
props = context.scene.swap_link_props
props.blend_path = self.filepath
obj_items, col_items = get_blend_objects_and_collections(self.filepath)
if not obj_items and not col_items:
self.report({'ERROR'}, "No objects or collections found in file")
return {'CANCELLED'}
object_names = [item[0] for item in obj_items]
collection_names = [item[0] for item in col_items]
# Auto-pick close match
if props.old_object:
close_obj = difflib.get_close_matches(props.old_object, object_names, 1)
close_col = difflib.get_close_matches(props.old_object, collection_names, 1)
if close_obj:
props.new_object = close_obj[0]
props.use_collection = False
elif close_col:
props.new_collection = close_col[0]
props.use_collection = True
return {'FINISHED'}
class VIEW3D_OT_swap_linked_asset(Operator):
bl_idname = "view3d.swap_linked_asset"
bl_label = "Swap Linked Asset"
def execute(self, context):
props = context.scene.swap_link_props
old_name = props.old_object
path = props.blend_path
if props.use_collection:
name = props.new_collection
if not name:
self.report({'ERROR'}, "No collection selected")
return {'CANCELLED'}
with bpy.data.libraries.load(path, link=True) as (data_from, data_to):
if name in data_from.collections:
data_to.collections = [name]
new_col = bpy.data.collections.get(name)
if not new_col:
self.report({'ERROR'}, "Failed to load collection")
return {'CANCELLED'}
# Make a copy list of all old objects to replace
old_objs = [obj for obj in bpy.data.objects if obj.name == old_name and obj.library]
replaced = 0
for obj in old_objs:
loc = obj.location.copy()
rot = obj.rotation_euler.copy()
scale = obj.scale.copy()
user_cols = list(obj.users_collection)
# Unlink and remove old object
for col in user_cols:
col.objects.unlink(obj)
bpy.data.objects.remove(obj, do_unlink=True)
# Create new collection instance
inst = bpy.data.objects.new(f"{name}_instance", None)
inst.instance_type = 'COLLECTION'
inst.instance_collection = new_col
inst.location = loc
inst.rotation_euler = rot
inst.scale = scale
for col in user_cols:
col.objects.link(inst)
replaced += 1
# Force depsgraph update (optional)
context.view_layer.update()
self.report({'INFO'}, f"Replaced with collection ({replaced})")
return {'FINISHED'}
else:
name = props.new_object
if not name:
self.report({'ERROR'}, "No object selected")
return {'CANCELLED'}
with bpy.data.libraries.load(path, link=True) as (data_from, data_to):
if name in data_from.objects:
data_to.objects = [name]
new_obj = bpy.data.objects.get(name)
if not new_obj:
self.report({'ERROR'}, "Failed to load object")
return {'CANCELLED'}
old_objs = [obj for obj in bpy.data.objects if obj.name == old_name and obj.library]
replaced = 0
for obj in old_objs:
loc = obj.location.copy()
rot = obj.rotation_euler.copy()
scale = obj.scale.copy()
user_cols = list(obj.users_collection)
# Unlink and remove old object
for col in user_cols:
col.objects.unlink(obj)
bpy.data.objects.remove(obj, do_unlink=True)
# Copy and link new object
inst = new_obj.copy()
inst.location = loc
inst.rotation_euler = rot
inst.scale = scale
for col in user_cols:
col.objects.link(inst)
replaced += 1
context.view_layer.update()
self.report({'INFO'}, f"Replaced with object ({replaced})")
return {'FINISHED'}
class VIEW3D_PT_swap_panel(Panel):
bl_label = "Relink Broken Asset"
bl_idname = "VIEW3D_PT_swap_panel"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Relink'
def draw(self, context):
layout = self.layout
props = context.scene.swap_link_props
broken_objs = [obj for obj in bpy.data.objects if is_broken_link(obj)]
if broken_objs:
layout.label(text="Broken Linked Objects:")
layout.prop_search(props, "old_object", bpy.data, "objects", text="")
else:
layout.label(text="No broken linked objects.")
layout.operator("view3d.select_blend_file", text="Select .blend File")
layout.prop(props, "use_collection")
if props.use_collection:
layout.prop(props, "new_collection")
else:
layout.prop(props, "new_object")
layout.operator("view3d.swap_linked_asset", text="Swap Asset")
classes = (
SwapLinkProps,
VIEW3D_OT_select_blend_file,
VIEW3D_OT_swap_linked_asset,
VIEW3D_PT_swap_panel,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.Scene.swap_link_props = PointerProperty(type=SwapLinkProps)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)
del bpy.types.Scene.swap_link_props
if __name__ == "__main__":
register()